231 lines
8.8 KiB
JavaScript
231 lines
8.8 KiB
JavaScript
globalThis.Wegwichtel ??= {};
|
|
(function (ns) {
|
|
'use strict';
|
|
const INTERVAL_MS = 120000;
|
|
let id = null;
|
|
let editingId = null;
|
|
let status = 'idle';
|
|
let timer = null;
|
|
let generation = 0;
|
|
let pendingGeneration = null;
|
|
let enabled = false;
|
|
let manualGeneration = 0;
|
|
let message = '';
|
|
let manualFields = [];
|
|
let lastPosition = null;
|
|
|
|
function render() {
|
|
const toggle = document.getElementById('route-recording-toggle');
|
|
if (toggle) { toggle.checked = enabled; toggle.setAttribute('aria-checked', String(enabled)); }
|
|
const control = document.getElementById('route-recording-control');
|
|
if (!control) return;
|
|
control.hidden = status === 'idle';
|
|
document.getElementById('route-recording-status').textContent = status === 'idle' ? '' : message || ns.I18n.t(`recorder.${status}`);
|
|
document.getElementById('route-recording-pause').textContent = ns.I18n.t(status === 'paused' ? 'recorder.resume' : 'recorder.pause');
|
|
}
|
|
|
|
async function sample() {
|
|
if (pendingGeneration === generation || status !== 'recording') return;
|
|
const currentGeneration = generation;
|
|
pendingGeneration = currentGeneration;
|
|
try {
|
|
let position;
|
|
try { position = await ns.Geo.current({ maximumAge: 0 }); }
|
|
catch { position = { timestamp: Date.now() }; }
|
|
if (currentGeneration !== generation || status !== 'recording') return;
|
|
lastPosition = Number.isFinite(position.lat) && Number.isFinite(position.lon) ? position : null;
|
|
const route = ns.LocalRoutes.get(id);
|
|
if (!route) { stop(); return; }
|
|
const gpx = ns.LocalRoutes.appendTrackPoint(route.gpx, position, route.route.name);
|
|
ns.LocalRoutes.save({ ...route, gpx });
|
|
message = ns.I18n.t(Number.isFinite(position.lat) && Number.isFinite(position.lon) ? 'recorder.recording' : 'recorder.no-location');
|
|
} catch (error) {
|
|
pause();
|
|
ns.StorageUsage?.reportError(error);
|
|
message = ns.I18n.t('recorder.save-error');
|
|
} finally {
|
|
if (pendingGeneration === currentGeneration) pendingGeneration = null;
|
|
render();
|
|
}
|
|
}
|
|
|
|
function clearTimer() {
|
|
if (timer !== null) globalThis.clearInterval(timer);
|
|
timer = null;
|
|
generation += 1;
|
|
}
|
|
|
|
function resume() {
|
|
if (!id) return;
|
|
clearTimer();
|
|
status = 'recording';
|
|
message = '';
|
|
timer = globalThis.setInterval(sample, INTERVAL_MS);
|
|
sample();
|
|
render();
|
|
}
|
|
|
|
function pause() {
|
|
clearTimer();
|
|
status = id ? 'paused' : 'idle';
|
|
message = '';
|
|
render();
|
|
}
|
|
|
|
function stop() {
|
|
enabled = false;
|
|
clearTimer();
|
|
id = null;
|
|
lastPosition = null;
|
|
status = 'idle';
|
|
message = '';
|
|
render();
|
|
}
|
|
|
|
function start(routeId) {
|
|
enabled = true;
|
|
lastPosition = null;
|
|
id = String(routeId);
|
|
editingId = id;
|
|
resume();
|
|
}
|
|
|
|
function beginEditing(routeId) {
|
|
const nextId = String(routeId);
|
|
if (editingId === nextId) return;
|
|
if (editingId !== null) endEditing();
|
|
editingId = nextId;
|
|
if (enabled) { id = editingId; resume(); }
|
|
render();
|
|
}
|
|
|
|
function setEnabled(value) {
|
|
if (!value) { stop(); return; }
|
|
enabled = true;
|
|
if (editingId) { id = editingId; resume(); }
|
|
render();
|
|
}
|
|
|
|
function endEditing() {
|
|
editingId = null;
|
|
manualGeneration += 1;
|
|
stop();
|
|
}
|
|
|
|
async function fillCurrentLocation(field, button, notice) {
|
|
if (button.disabled) return;
|
|
const currentGeneration = manualGeneration;
|
|
button.disabled = true;
|
|
notice.textContent = ns.I18n.t('recorder.locating');
|
|
try {
|
|
const position = await ns.Geo.current({ maximumAge: 0 });
|
|
if (currentGeneration !== manualGeneration || !manualFields.includes(field)) return;
|
|
if (!Number.isFinite(position.lat) || !Number.isFinite(position.lon) || Math.abs(position.lat) > 90 || Math.abs(position.lon) > 180) throw new Error('Invalid location');
|
|
field.lat.value = String(position.lat);
|
|
field.lon.value = String(position.lon);
|
|
notice.textContent = ns.I18n.t('recorder.location-set');
|
|
} catch {
|
|
if (currentGeneration === manualGeneration && manualFields.includes(field)) notice.textContent = ns.I18n.t('recorder.location-error');
|
|
} finally {
|
|
button.disabled = false;
|
|
}
|
|
}
|
|
|
|
function addPositionField(position = {}, index = null) {
|
|
const container = document.getElementById('editor-recorded-positions');
|
|
if (!container) return;
|
|
const row = document.createElement('fieldset');
|
|
const legend = document.createElement('legend');
|
|
legend.textContent = ns.I18n.t('recorder.position-number', { number: manualFields.length + 1 });
|
|
row.appendChild(legend);
|
|
const fields = {};
|
|
for (const [key, textKey, minimum, maximum] of [['lat', 'latitude', -90, 90], ['lon', 'longitude', -180, 180]]) {
|
|
const label = document.createElement('label');
|
|
const input = document.createElement('input');
|
|
input.id = `recorded-position-${manualFields.length}-${key}`;
|
|
label.setAttribute('for', input.id);
|
|
label.textContent = ns.I18n.t(`recorder.${textKey}`);
|
|
input.type = 'number'; input.step = 'any'; input.min = minimum; input.max = maximum;
|
|
input.value = position[key] == null ? '' : String(position[key]);
|
|
row.appendChild(label); row.appendChild(input);
|
|
fields[key] = input;
|
|
}
|
|
const field = { index, original: position, ...fields };
|
|
manualFields.push(field);
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'current-location-button';
|
|
button.setAttribute('aria-label', ns.I18n.t('recorder.use-location'));
|
|
button.title = ns.I18n.t('recorder.use-location');
|
|
const icon = document.createElement('img');
|
|
icon.src = '/images/icons/current-location.svg';
|
|
icon.alt = '';
|
|
button.appendChild(icon);
|
|
const notice = document.createElement('p');
|
|
notice.className = 'editor-muted';
|
|
notice.setAttribute('aria-live', 'polite');
|
|
button.addEventListener('click', () => { void fillCurrentLocation(field, button, notice); });
|
|
row.appendChild(button);
|
|
row.appendChild(notice);
|
|
container.appendChild(row);
|
|
}
|
|
|
|
function renderPoints(gpx) {
|
|
const container = document.getElementById('editor-recorded-positions');
|
|
if (!container) return;
|
|
manualGeneration += 1;
|
|
container.replaceChildren();
|
|
manualFields = [];
|
|
ns.LocalRoutes.recordedPositions(gpx).forEach(addPositionField);
|
|
}
|
|
|
|
function editGpx(gpx, routeName) {
|
|
let changed = false;
|
|
const positions = ns.LocalRoutes.recordedPositions(gpx);
|
|
for (const field of manualFields) {
|
|
const values = ['lat', 'lon'].map(key => field[key].value.trim() === '' ? null : Number(field[key].value));
|
|
const [lat, lon] = values;
|
|
if ((lat === null) !== (lon === null) || (lat !== null && (!Number.isFinite(lat) || !Number.isFinite(lon) || Math.abs(lat) > 90 || Math.abs(lon) > 180))) {
|
|
throw new Error(ns.I18n.t('editor.error.station-location'));
|
|
}
|
|
if (field.index !== null && lat === field.original.lat && lon === field.original.lon) continue;
|
|
const point = { ...field.original, lat, lon };
|
|
if (field.index === null) positions.push(point);
|
|
else positions[field.index] = point;
|
|
changed = true;
|
|
}
|
|
return changed ? ns.LocalRoutes.replaceRecordedPositions(gpx, positions, routeName) : gpx;
|
|
}
|
|
|
|
function bind() {
|
|
globalThis.addEventListener?.('pagehide', endEditing);
|
|
document.getElementById('route-recording-toggle')?.addEventListener('change', event => setEnabled(event.target.checked));
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.visibilityState === 'visible' && status === 'recording') void sample();
|
|
});
|
|
const activate = (elementId, action) => {
|
|
const element = document.getElementById(elementId);
|
|
element?.addEventListener('pointerup', event => {
|
|
if (event.isPrimary === false || (event.pointerType === 'mouse' && event.button !== 0)) return;
|
|
event.preventDefault();
|
|
action();
|
|
});
|
|
element?.addEventListener('keydown', event => {
|
|
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
event.preventDefault();
|
|
action();
|
|
});
|
|
};
|
|
activate('route-recording-pause', () => status === 'recording' ? pause() : resume());
|
|
activate('route-recording-stop', stop);
|
|
activate('editor-add-position', () => addPositionField({ timestamp: new Date().toISOString() }));
|
|
document.addEventListener('wegwichtel:local-routes-changed', () => {
|
|
if (id && !ns.LocalRoutes.get(id)) stop();
|
|
});
|
|
}
|
|
|
|
ns.RouteRecorder = Object.freeze({ start, pause, resume, stop, beginEditing, endEditing, renderPoints, editGpx, get status() { return status; }, get routeId() { return id; }, get position() { return lastPosition; } });
|
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bind, { once: true });
|
|
else bind();
|
|
}(globalThis.Wegwichtel));
|