reworked app screens and added vibration
This commit is contained in:
+125
-27
@@ -155,6 +155,7 @@
|
||||
function stopRouteSensors() {
|
||||
ns.Geo.stop();
|
||||
ns.Orientation.stop();
|
||||
ns.Vibration.stop();
|
||||
state.deviceHeading = null;
|
||||
}
|
||||
|
||||
@@ -242,20 +243,27 @@
|
||||
<button type="button" data-poi-id="${poi.id}">
|
||||
<strong>${escapeHtml(poi.title)}</strong>
|
||||
<span>${escapeHtml(poi.description || '')}</span>
|
||||
<span class="poi-proximity" aria-label="Entfernung wird ermittelt">
|
||||
<svg viewBox="0 0 32 32" aria-hidden="true" focusable="false">
|
||||
<path d="M16 2 27 28 16 23 5 28Z"/>
|
||||
</svg>
|
||||
<small>–</small>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
`);
|
||||
});
|
||||
|
||||
updatePoiIndicators(state.position);
|
||||
}
|
||||
|
||||
function showPoi(poi, automatic) {
|
||||
function showPoi(poi) {
|
||||
setActivePoi(poi.id);
|
||||
$('#poi-title').text(poi.title);
|
||||
$('#poi-description').text(poi.description || '');
|
||||
ns.Slideshow.show(poi.images);
|
||||
ns.AudioPlayer.load(poi.audioUrl);
|
||||
navigate('#poi-page');
|
||||
if (automatic && poi.audioUrl) ns.AudioPlayer.play().catch(() => {});
|
||||
}
|
||||
|
||||
function nearestRoutePointIndex(position) {
|
||||
@@ -282,12 +290,63 @@
|
||||
return nearestIndex;
|
||||
}
|
||||
|
||||
function updateNavigation(position) {
|
||||
function effectiveHeading(position) {
|
||||
const movementHeading = position
|
||||
&& Number.isFinite(position.heading)
|
||||
&& Number(position.speed) > 0.5
|
||||
? position.heading
|
||||
: null;
|
||||
return state.deviceHeading ?? movementHeading;
|
||||
}
|
||||
|
||||
function poiMetrics(position) {
|
||||
if (!position || !state.route?.pois?.length) return [];
|
||||
const heading = effectiveHeading(position);
|
||||
|
||||
return state.route.pois.map(poi => {
|
||||
const distance = ns.Distance.meters(position, poi);
|
||||
const bearing = ns.Distance.bearing(position, poi);
|
||||
return {
|
||||
poi,
|
||||
distance,
|
||||
bearing,
|
||||
rotation: heading == null
|
||||
? bearing
|
||||
: ns.Distance.normalizeDegrees(bearing - heading)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function updatePoiIndicators(position) {
|
||||
const metrics = poiMetrics(position);
|
||||
|
||||
metrics.forEach(metric => {
|
||||
const indicator = $(`#poi-list [data-poi-id="${metric.poi.id}"] .poi-proximity`);
|
||||
const distance = Math.round(metric.distance);
|
||||
indicator
|
||||
.attr('aria-label', `${distance} Meter entfernt`)
|
||||
.attr('title', `${distance} m entfernt`);
|
||||
indicator.find('svg').css('transform', `rotate(${metric.rotation.toFixed(1)}deg)`);
|
||||
indicator.find('small').text(`${distance} m`);
|
||||
});
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
function nearestPendingPoi(metrics) {
|
||||
return metrics
|
||||
.filter(metric => !state.triggered.has(metric.poi.id))
|
||||
.reduce((nearest, metric) => (
|
||||
!nearest || metric.distance < nearest.distance ? metric : nearest
|
||||
), null);
|
||||
}
|
||||
|
||||
function updateRouteProgress(position) {
|
||||
const points = state.route?.points || [];
|
||||
if (state.routeState !== 'running' || !points.length) return;
|
||||
if (!points.length) return null;
|
||||
|
||||
const nearestIndex = nearestRoutePointIndex(position);
|
||||
if (nearestIndex == null) return;
|
||||
if (nearestIndex == null) return null;
|
||||
|
||||
state.routeProgressIndex = state.routeProgressIndex == null
|
||||
? nearestIndex
|
||||
@@ -308,32 +367,72 @@
|
||||
|
||||
if (targetIndex === lastIndex && distance <= reachedRadius) {
|
||||
state.routeProgressIndex = lastIndex;
|
||||
return { reached: true, target, distance: 0 };
|
||||
}
|
||||
|
||||
return { reached: false, target, distance };
|
||||
}
|
||||
|
||||
function showPoiNavigation(position, metric) {
|
||||
const heading = effectiveHeading(position);
|
||||
const distance = Math.round(metric.distance);
|
||||
|
||||
$('#navigation-arrow').css('transform', `rotate(${metric.rotation.toFixed(1)}deg)`);
|
||||
showNavigation(
|
||||
heading == null
|
||||
? `Norden oben · nächster POI: ${metric.poi.title}`
|
||||
: `Nächster POI: ${metric.poi.title}`,
|
||||
`${distance} m`
|
||||
);
|
||||
$('#navigation-footer').attr(
|
||||
'aria-label',
|
||||
`Nächster POI ${metric.poi.title} in ${distance} Metern`
|
||||
);
|
||||
}
|
||||
|
||||
function showRoutePointNavigation(position, progress) {
|
||||
if (!progress) {
|
||||
$('#navigation-arrow').css('transform', 'rotate(0deg)');
|
||||
showNavigation('Ziel der Route erreicht', '0 m');
|
||||
$('#navigation-footer').attr('aria-label', 'Ziel der Route erreicht');
|
||||
showNavigation('Keine Navigationspunkte vorhanden', '–');
|
||||
return;
|
||||
}
|
||||
|
||||
const bearing = ns.Distance.bearing(position, target);
|
||||
const movementHeading = Number.isFinite(position.heading) && Number(position.speed) > 0.5
|
||||
? position.heading
|
||||
: null;
|
||||
const heading = state.deviceHeading ?? movementHeading;
|
||||
if (progress.reached) {
|
||||
$('#navigation-arrow').css('transform', 'rotate(0deg)');
|
||||
showNavigation('Alle Stationen und das Routenziel erreicht', '0 m');
|
||||
$('#navigation-footer').attr('aria-label', 'Alle Stationen und das Routenziel erreicht');
|
||||
return;
|
||||
}
|
||||
|
||||
const bearing = ns.Distance.bearing(position, progress.target);
|
||||
const heading = effectiveHeading(position);
|
||||
const rotation = heading == null
|
||||
? bearing
|
||||
: ns.Distance.normalizeDegrees(bearing - heading);
|
||||
const distance = Math.round(progress.distance);
|
||||
|
||||
$('#navigation-arrow').css('transform', `rotate(${rotation.toFixed(1)}deg)`);
|
||||
showNavigation(
|
||||
heading == null ? 'Norden oben · nächster Routenpunkt' : 'Nächster Routenpunkt',
|
||||
`${Math.round(distance)} m`
|
||||
`${distance} m`
|
||||
);
|
||||
$('#navigation-footer').attr(
|
||||
'aria-label',
|
||||
`Nächster Routenpunkt in ${Math.round(distance)} Metern`
|
||||
`Nächster Routenpunkt in ${distance} Metern`
|
||||
);
|
||||
}
|
||||
|
||||
function updateNavigation(position) {
|
||||
if (state.routeState !== 'running') return;
|
||||
|
||||
const progress = updateRouteProgress(position);
|
||||
const metrics = updatePoiIndicators(position);
|
||||
const nextPoi = nearestPendingPoi(metrics);
|
||||
|
||||
if (nextPoi) showPoiNavigation(position, nextPoi);
|
||||
else showRoutePointNavigation(position, progress);
|
||||
}
|
||||
|
||||
function updateHeading(heading) {
|
||||
if (state.deviceHeading == null) {
|
||||
state.deviceHeading = heading;
|
||||
@@ -347,26 +446,25 @@
|
||||
|
||||
function evaluatePosition(position) {
|
||||
state.position = position;
|
||||
if (!state.route || state.routeState !== 'running') return;
|
||||
if (!state.route) return;
|
||||
|
||||
const metrics = updatePoiIndicators(position);
|
||||
if (state.routeState !== 'running') return;
|
||||
|
||||
let nearest = nearestPendingPoi(metrics);
|
||||
updateNavigation(position);
|
||||
|
||||
let nearest = null;
|
||||
state.route.pois.forEach(poi => {
|
||||
if (state.triggered.has(poi.id)) return;
|
||||
const distance = ns.Distance.meters(position, poi);
|
||||
if (!nearest || distance < nearest.distance) nearest = { poi, distance };
|
||||
});
|
||||
|
||||
$('#tracking-status').text(
|
||||
`GPS aktiv · Genauigkeit ${Math.round(position.accuracy)} m`
|
||||
+ (nearest ? ` · nächste Station ${Math.round(nearest.distance)} m` : '')
|
||||
+ (nearest ? ` · nächster POI ${Math.round(nearest.distance)} m` : ' · alle POIs erreicht')
|
||||
);
|
||||
|
||||
if (nearest && nearest.distance <= nearest.poi.triggerRadiusM) {
|
||||
state.triggered.add(nearest.poi.id);
|
||||
if (navigator.vibrate) navigator.vibrate([120, 80, 120]);
|
||||
showPoi(nearest.poi, true);
|
||||
ns.Vibration.start(ns.Vibration.Patterns.ACTIVE_POI);
|
||||
showPoi(nearest.poi);
|
||||
nearest = nearestPendingPoi(metrics);
|
||||
updateNavigation(position);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,7 +497,7 @@
|
||||
}
|
||||
|
||||
if (!orientationAvailable) {
|
||||
$('#navigation-mode').text('Norden oben · nächster Routenpunkt');
|
||||
$('#navigation-mode').text('Norden oben · Navigation');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,7 +524,7 @@
|
||||
|
||||
activate('[data-poi-id]', function () {
|
||||
const poi = state.route?.pois.find(item => item.id === Number($(this).data('poi-id')));
|
||||
if (poi) showPoi(poi, false);
|
||||
if (poi) showPoi(poi);
|
||||
});
|
||||
|
||||
activate('[data-page-target]', function () {
|
||||
|
||||
Vendored
+1
-1
@@ -70,7 +70,7 @@
|
||||
initializeJqueryUi();
|
||||
mark('jquery-ui', 'done');
|
||||
|
||||
for (const module of ['config', 'api-client', 'distance', 'geolocation', 'orientation', 'slideshow', 'audio-player', 'app']) {
|
||||
for (const module of ['config', 'api-client', 'distance', 'geolocation', 'orientation', 'slideshow', 'audio-player', 'vibration', 'app']) {
|
||||
await loadScript(`js/${module}.js`, () => Boolean(window.Wegwichtel));
|
||||
}
|
||||
mark('modules', 'done');
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
(function (ns) {
|
||||
'use strict';
|
||||
|
||||
const Patterns = Object.freeze({
|
||||
MICRO: 50,
|
||||
MINI: 100,
|
||||
SHORT: 200,
|
||||
LONG: 2000,
|
||||
ALARM: Object.freeze([100, 50, 100]),
|
||||
POI: Object.freeze([2000, 200, 2000]),
|
||||
ACTIVE_POI: Object.freeze([500, 300, 500])
|
||||
});
|
||||
|
||||
function supported() {
|
||||
return typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function';
|
||||
}
|
||||
|
||||
function normalize(pattern) {
|
||||
const selected = pattern ?? Patterns.LONG;
|
||||
|
||||
if (Array.isArray(selected)) {
|
||||
const values = selected.map(Number);
|
||||
if (!values.length || values.some(value => !Number.isFinite(value) || value < 0)) {
|
||||
throw new TypeError('Das Vibrationsmuster muss aus nichtnegativen Millisekunden bestehen.');
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
const duration = Number(selected);
|
||||
if (!Number.isFinite(duration) || duration < 0) {
|
||||
throw new TypeError('Die Vibrationsdauer muss eine nichtnegative Millisekundenangabe sein.');
|
||||
}
|
||||
return duration;
|
||||
}
|
||||
|
||||
function start(pattern = Patterns.LONG) {
|
||||
const normalized = normalize(pattern);
|
||||
if (!supported()) return false;
|
||||
return navigator.vibrate(normalized);
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (!supported()) return false;
|
||||
return navigator.vibrate(0);
|
||||
}
|
||||
|
||||
ns.Vibration = Object.freeze({
|
||||
Patterns,
|
||||
isSupported: supported,
|
||||
start,
|
||||
stop
|
||||
});
|
||||
}(window.Wegwichtel));
|
||||
Reference in New Issue
Block a user