Files
Wegwichtel/public/js/route-recorder.js
T
Florian Zumpe 171ea88d55
Sonarqube Scanner / Prepare environment (push) Successful in 8s
Sonarqube Scanner / Build and test (push) Failing after 22s
Sonarqube Scanner / SonarQube analysis (push) Skipped
Sonarqube Scanner / Deploy to Server (push) Successful in 6s
overhaul gpx creation and usage
2026-10-03 12:40:26 +02:00

160 lines
5.9 KiB
JavaScript

globalThis.Wegwichtel ??= {};
(function (ns) {
'use strict';
const INTERVAL_MS = 30000;
let id = null;
let status = 'idle';
let timer = null;
let generation = 0;
let pending = false;
let message = '';
let manualFields = [];
let lastPosition = null;
function render() {
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 (pending || status !== 'recording') return;
pending = true;
const currentGeneration = generation;
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 {
pending = false;
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() {
clearTimer();
id = null;
lastPosition = null;
status = 'idle';
message = '';
render();
}
function start(routeId) {
lastPosition = null;
id = String(routeId);
resume();
}
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;
}
manualFields.push({ index, original: position, ...fields });
container.appendChild(row);
}
function renderPoints(gpx) {
const container = document.getElementById('editor-recorded-positions');
if (!container) return;
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() {
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, 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));