overhaul gpx creation and usage
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

This commit is contained in:
Florian Zumpe
2026-10-03 12:40:26 +02:00
parent 1cb3977269
commit 171ea88d55
29 changed files with 969 additions and 344 deletions
+33 -10
View File
@@ -3,7 +3,7 @@ import path from 'node:path';
import { transaction } from '../database.js';
import { config } from '../config.js';
import { HttpError } from '../middleware/errors.js';
import { parseGpx } from './gpx.js';
import { parseGpx, gpxWithWaypoints } from './gpx.js';
import { distanceMeters, routeMetrics } from './geo.js';
import {
ensureRouteDirectories,
@@ -203,6 +203,7 @@ export function createRoute(db, fields, gpxFile) {
return id;
});
ensureRouteDirectories(routeId);
fs.writeFileSync(gpxFile.path, parsed.normalizedXml);
const gpxPath = moveUploadedFile(gpxFile, path.join(routeDirectory(routeId), 'route.gpx'));
db.prepare('UPDATE routes SET gpx_path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
.run(gpxPath, routeId);
@@ -231,7 +232,11 @@ export function updateRoute(db, id, fields, gpxFile) {
updateMetrics(db, id, parsed.metrics);
});
if (gpxFile) moveUploadedFile(gpxFile, path.join(routeDirectory(id), 'route.gpx'));
if (gpxFile) {
fs.writeFileSync(gpxFile.path, parsed.normalizedXml);
moveUploadedFile(gpxFile, path.join(routeDirectory(id), 'route.gpx'));
syncRouteWaypoints(db, id);
}
return getRoute(db, id);
}
@@ -250,6 +255,9 @@ export function appendRoute(db, id, gpxFile) {
});
removeUpload(gpxFile);
const target = resolveStoredFile(db.prepare('SELECT gpx_path FROM routes WHERE id = ?').get(id).gpx_path);
const original = fs.readFileSync(target, 'utf8');
fs.writeFileSync(target, parseGpx(original, allPoints).normalizedXml);
return getRoute(db, id);
}
@@ -283,15 +291,28 @@ export function getPoi(db, routeId, poiId) {
return listPois(db, routeId).find(poi => poi.id === Number(poiId));
}
function syncRouteWaypoints(db, routeId) {
const row = db.prepare('SELECT gpx_path FROM routes WHERE id = ?').get(routeId);
const filename = resolveStoredFile(row.gpx_path);
const xml = fs.readFileSync(filename, 'utf8');
fs.writeFileSync(filename, gpxWithWaypoints(xml, listPois(db, routeId)));
}
function poiCoordinates(fields, existing = {}) {
const values = ['lat', 'lon'].map(key => Object.hasOwn(fields, key) ? fields[key] : existing[key]);
const [lat, lon] = values.map(value => value == null || value === '' ? null : Number.parseFloat(value));
const absent = lat === null && lon === null;
if (!absent && (!Number.isFinite(lat) || !Number.isFinite(lon) || Math.abs(lat) > 90 || Math.abs(lon) > 180)) {
throw new HttpError(400, 'Gültige POI-Koordinaten sind erforderlich.');
}
return { lat, lon };
}
export function createPoi(db, routeId, fields) {
const route = db.prepare("SELECT id FROM routes WHERE id = ? AND status = 'active'").get(routeId);
if (!route) throw new HttpError(404, 'Strecke nicht gefunden.');
const lat = Number.parseFloat(fields.lat);
const lon = Number.parseFloat(fields.lon);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
throw new HttpError(400, 'Gültige POI-Koordinaten sind erforderlich.');
}
const { lat, lon } = poiCoordinates(fields);
const result = db.prepare(`INSERT INTO pois
(route_id, title, description, lat, lon, trigger_radius_m, sequence) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(
routeId,
@@ -302,6 +323,7 @@ export function createPoi(db, routeId, fields) {
Number.parseFloat(fields.triggerRadiusM || config.defaultPoiTriggerMeters),
Number.parseInt(fields.sequence || '0', 10)
);
syncRouteWaypoints(db, routeId);
return listPois(db, routeId).find(poi => poi.id === Number(result.lastInsertRowid));
}
@@ -309,13 +331,12 @@ export function updatePoi(db, routeId, poiId, fields) {
const existing = db.prepare(`SELECT p.*, r.status FROM pois p JOIN routes r ON r.id = p.route_id
WHERE p.id = ? AND p.route_id = ?`).get(poiId, routeId);
if (existing?.status !== 'active') throw new HttpError(404, 'POI nicht gefunden.');
const lat = fields.lat == null ? existing.lat : Number.parseFloat(fields.lat);
const lon = fields.lon == null ? existing.lon : Number.parseFloat(fields.lon);
const { lat, lon } = poiCoordinates(fields, existing);
const radius = fields.triggerRadiusM == null
? existing.trigger_radius_m
: Number.parseFloat(fields.triggerRadiusM);
const sequence = fields.sequence == null ? existing.sequence : Number.parseInt(fields.sequence, 10);
if (![lat, lon, radius].every(Number.isFinite) || !Number.isInteger(sequence) || sequence < 0) {
if (!Number.isFinite(radius) || !Number.isInteger(sequence) || sequence < 0) {
throw new HttpError(400, 'POI-Koordinaten, Auslöseradius oder Reihenfolge sind ungültig.');
}
db.prepare(`UPDATE pois SET title=?, description=?, lat=?, lon=?, trigger_radius_m=?, sequence=?,
@@ -328,6 +349,7 @@ export function updatePoi(db, routeId, poiId, fields) {
sequence,
poiId
);
syncRouteWaypoints(db, existing.route_id);
return listPois(db, existing.route_id).find(poi => poi.id === Number(poiId));
}
@@ -341,6 +363,7 @@ export function deletePoi(db, routeId, poiId) {
db.prepare('DELETE FROM pois WHERE id = ? AND route_id = ?').run(poiId, routeId);
});
syncRouteWaypoints(db, routeId);
images.forEach(image => removeStoredFile(image.path));
removeStoredFile(existing.audio_path);
return { id: poiId, routeId, deleted: true };