renew schema and tests for new dtabases
This commit is contained in:
@@ -4,7 +4,7 @@ import { transaction } from '../database.js';
|
||||
import { config } from '../config.js';
|
||||
import { HttpError } from '../middleware/errors.js';
|
||||
import { parseGpx, gpxWithWaypoints } from './gpx.js';
|
||||
import { distanceMeters, routeMetrics } from './geo.js';
|
||||
import { distanceMeters } from './geo.js';
|
||||
import {
|
||||
ensureRouteDirectories,
|
||||
moveUploadedFile,
|
||||
@@ -16,10 +16,13 @@ import {
|
||||
removeStoredFile
|
||||
} from './storage.js';
|
||||
|
||||
const INSERT_POINT_SQL = 'INSERT INTO route_points (route_id, sequence, lat, lon, elevation, recorded_at) VALUES (?, ?, ?, ?, ?, ?)';
|
||||
const UPDATE_METRICS_SQL = `UPDATE routes SET
|
||||
start_lat=?, start_lon=?, center_lat=?, center_lon=?, min_lat=?, min_lon=?, max_lat=?, max_lon=?,
|
||||
distance_m=?, elevation_gain_m=?, point_count=?, updated_at=CURRENT_TIMESTAMP WHERE id=?`;
|
||||
const UPDATE_BOUNDS_SQL = `UPDATE routes SET
|
||||
min_lat=?, min_lon=?, max_lat=?, max_lon=?, updated_at=CURRENT_TIMESTAMP WHERE id=?`;
|
||||
|
||||
function readRouteGpx(row) {
|
||||
const filename = resolveStoredFile(row.gpx_path, { allowTrash: row.status === 'deleted' });
|
||||
return parseGpx(fs.readFileSync(filename, 'utf8'), null, { allowEmpty: true });
|
||||
}
|
||||
|
||||
function trimEdgeHyphens(value) {
|
||||
let start = 0;
|
||||
@@ -41,8 +44,9 @@ const routeGpxUrl = routeId => `/api/routes/${routeId}/gpx`;
|
||||
const pictureUrl = (routeId, poiId, pictureId) => `/api/routes/${routeId}/pois/${poiId}/pictures/${pictureId}`;
|
||||
const audioUrl = (routeId, poiId) => `/api/routes/${routeId}/pois/${poiId}/audio`;
|
||||
|
||||
function rowToRoute(row, includeDeleted = false) {
|
||||
function rowToRoute(row, includeDeleted = false, parsed = null) {
|
||||
if (!row || (!includeDeleted && row.status !== 'active')) return null;
|
||||
const { metrics } = parsed ?? readRouteGpx(row);
|
||||
return {
|
||||
id: row.id,
|
||||
slug: row.slug,
|
||||
@@ -51,14 +55,14 @@ function rowToRoute(row, includeDeleted = false) {
|
||||
schoolName: row.school_name,
|
||||
status: row.status,
|
||||
activatedAt: row.activated_at,
|
||||
start: row.start_lat == null ? null : { lat: row.start_lat, lon: row.start_lon },
|
||||
center: row.center_lat == null ? null : { lat: row.center_lat, lon: row.center_lon },
|
||||
start: metrics.startLat == null ? null : { lat: metrics.startLat, lon: metrics.startLon },
|
||||
center: metrics.centerLat == null ? null : { lat: metrics.centerLat, lon: metrics.centerLon },
|
||||
bounds: row.min_lat == null
|
||||
? null
|
||||
: { minLat: row.min_lat, minLon: row.min_lon, maxLat: row.max_lat, maxLon: row.max_lon },
|
||||
distanceM: row.distance_m,
|
||||
elevationGainM: row.elevation_gain_m,
|
||||
pointCount: row.point_count,
|
||||
distanceM: metrics.distanceM,
|
||||
elevationGainM: metrics.elevationGainM,
|
||||
pointCount: metrics.pointCount,
|
||||
gpxUrl: row.gpx_path && row.status === 'active' ? routeGpxUrl(row.id) : null,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
@@ -71,26 +75,22 @@ function routeComparator(origin) {
|
||||
return (left, right) => left.name.localeCompare(right.name, 'de');
|
||||
}
|
||||
|
||||
function insertRoutePoints(db, routeId, points, offset = 0) {
|
||||
const insert = db.prepare(INSERT_POINT_SQL);
|
||||
points.forEach((point, index) => {
|
||||
insert.run(routeId, offset + index, point.lat, point.lon, point.elevation, point.recordedAt);
|
||||
});
|
||||
function withinRouteBounds(row, origin, maximumDistance) {
|
||||
if (!origin) return true;
|
||||
if (row.min_lat == null || row.min_lon == null || row.max_lat == null || row.max_lon == null) return false;
|
||||
const nearest = {
|
||||
lat: Math.max(row.min_lat, Math.min(row.max_lat, origin.lat)),
|
||||
lon: Math.max(row.min_lon, Math.min(row.max_lon, origin.lon))
|
||||
};
|
||||
return distanceMeters(origin, nearest) <= maximumDistance;
|
||||
}
|
||||
|
||||
function updateMetrics(db, routeId, metrics) {
|
||||
db.prepare(UPDATE_METRICS_SQL).run(
|
||||
metrics.startLat,
|
||||
metrics.startLon,
|
||||
metrics.centerLat,
|
||||
metrics.centerLon,
|
||||
function updateBounds(db, routeId, metrics) {
|
||||
db.prepare(UPDATE_BOUNDS_SQL).run(
|
||||
metrics.minLat,
|
||||
metrics.minLon,
|
||||
metrics.maxLat,
|
||||
metrics.maxLon,
|
||||
metrics.distanceM,
|
||||
metrics.elevationGainM,
|
||||
metrics.pointCount,
|
||||
routeId
|
||||
);
|
||||
}
|
||||
@@ -109,7 +109,6 @@ function uniqueSlug(db, source) {
|
||||
function cleanupFailedRoute(db, routeId, gpxFile) {
|
||||
removeUpload(gpxFile);
|
||||
if (!routeId) return;
|
||||
db.prepare('DELETE FROM route_points WHERE route_id = ?').run(routeId);
|
||||
db.prepare('DELETE FROM routes WHERE id = ?').run(routeId);
|
||||
fs.rmSync(routeDirectory(routeId), { recursive: true, force: true });
|
||||
}
|
||||
@@ -139,22 +138,22 @@ export function listRoutes(db, {
|
||||
const origin = Number.isFinite(lat) && Number.isFinite(lon) ? { lat, lon } : null;
|
||||
const maximumDistance = radiusKm * 1000;
|
||||
return rows
|
||||
.filter(row => withinRouteBounds(row, origin, maximumDistance))
|
||||
.map(row => {
|
||||
const route = rowToRoute(row, includeDeleted);
|
||||
const proximityM = origin && route?.start ? distanceMeters(origin, route.start) : null;
|
||||
return { ...route, proximityM };
|
||||
})
|
||||
.filter(route => !origin || route.proximityM <= maximumDistance)
|
||||
.filter(route => !origin || (route.proximityM != null && route.proximityM <= maximumDistance))
|
||||
.sort(routeComparator(origin));
|
||||
}
|
||||
|
||||
export function getRoute(db, id, includeDeleted = false) {
|
||||
const row = db.prepare('SELECT * FROM routes WHERE id = ?').get(id);
|
||||
const route = rowToRoute(row, includeDeleted);
|
||||
if (!route) throw new HttpError(404, 'Strecke nicht gefunden.');
|
||||
|
||||
const points = db.prepare(`SELECT sequence, lat, lon, elevation, recorded_at AS recordedAt
|
||||
FROM route_points WHERE route_id = ? ORDER BY sequence`).all(id);
|
||||
if (!row || (!includeDeleted && row.status !== 'active')) throw new HttpError(404, 'Strecke nicht gefunden.');
|
||||
const parsed = readRouteGpx(row);
|
||||
const route = rowToRoute(row, includeDeleted, parsed);
|
||||
const { points } = parsed;
|
||||
return { ...route, points, pois: listPois(db, id) };
|
||||
}
|
||||
|
||||
@@ -178,28 +177,19 @@ export function createRoute(db, fields, gpxFile) {
|
||||
const slug = uniqueSlug(db, fields.slug || name);
|
||||
const metrics = parsed.metrics;
|
||||
const result = db.prepare(`INSERT INTO routes
|
||||
(slug, name, description, school_name, gpx_path, start_lat, start_lon, center_lat, center_lon,
|
||||
min_lat, min_lon, max_lat, max_lon, distance_m, elevation_gain_m, point_count)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
||||
(slug, name, description, school_name, gpx_path, min_lat, min_lon, max_lat, max_lon)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
||||
slug,
|
||||
name,
|
||||
fields.description || '',
|
||||
fields.schoolName || '',
|
||||
'pending',
|
||||
metrics.startLat,
|
||||
metrics.startLon,
|
||||
metrics.centerLat,
|
||||
metrics.centerLon,
|
||||
metrics.minLat,
|
||||
metrics.minLon,
|
||||
metrics.maxLat,
|
||||
metrics.maxLon,
|
||||
metrics.distanceM,
|
||||
metrics.elevationGainM,
|
||||
metrics.pointCount
|
||||
metrics.maxLon
|
||||
);
|
||||
const id = Number(result.lastInsertRowid);
|
||||
insertRoutePoints(db, id, parsed.points);
|
||||
return id;
|
||||
});
|
||||
ensureRouteDirectories(routeId);
|
||||
@@ -226,10 +216,6 @@ export function updateRoute(db, id, fields, gpxFile) {
|
||||
fields.schoolName ?? existing.school_name,
|
||||
id
|
||||
);
|
||||
if (!parsed) return;
|
||||
db.prepare('DELETE FROM route_points WHERE route_id = ?').run(id);
|
||||
insertRoutePoints(db, id, parsed.points);
|
||||
updateMetrics(db, id, parsed.metrics);
|
||||
});
|
||||
|
||||
if (gpxFile) {
|
||||
@@ -242,22 +228,18 @@ export function updateRoute(db, id, fields, gpxFile) {
|
||||
|
||||
export function appendRoute(db, id, gpxFile) {
|
||||
if (!gpxFile) throw new HttpError(400, 'Eine GPX-Datei ist erforderlich.');
|
||||
const existing = db.prepare("SELECT id FROM routes WHERE id = ? AND status = 'active'").get(id);
|
||||
const existing = db.prepare("SELECT * FROM routes WHERE id = ? AND status = 'active'").get(id);
|
||||
if (!existing) throw new HttpError(404, 'Strecke nicht gefunden.');
|
||||
|
||||
const parsed = parseGpx(fs.readFileSync(gpxFile.path, 'utf8'));
|
||||
const current = db.prepare(`SELECT lat, lon, elevation, recorded_at AS recordedAt
|
||||
FROM route_points WHERE route_id = ? ORDER BY sequence`).all(id);
|
||||
const current = readRouteGpx(existing).points;
|
||||
const allPoints = current.concat(parsed.points).map((point, sequence) => ({ ...point, sequence }));
|
||||
transaction(db, () => {
|
||||
insertRoutePoints(db, id, parsed.points, current.length);
|
||||
updateMetrics(db, id, routeMetrics(allPoints));
|
||||
});
|
||||
|
||||
removeUpload(gpxFile);
|
||||
const target = resolveStoredFile(db.prepare('SELECT gpx_path FROM routes WHERE id = ?').get(id).gpx_path);
|
||||
const target = resolveStoredFile(existing.gpx_path);
|
||||
const original = fs.readFileSync(target, 'utf8');
|
||||
fs.writeFileSync(target, parseGpx(original, allPoints).normalizedXml);
|
||||
const normalized = parseGpx(original, allPoints);
|
||||
fs.writeFileSync(target, normalized.normalizedXml);
|
||||
updateBounds(db, id, normalized.metrics);
|
||||
removeUpload(gpxFile);
|
||||
return getRoute(db, id);
|
||||
}
|
||||
|
||||
@@ -296,6 +278,7 @@ function syncRouteWaypoints(db, routeId) {
|
||||
const filename = resolveStoredFile(row.gpx_path);
|
||||
const xml = fs.readFileSync(filename, 'utf8');
|
||||
fs.writeFileSync(filename, gpxWithWaypoints(xml, listPois(db, routeId)));
|
||||
updateBounds(db, routeId, parseGpx(fs.readFileSync(filename, 'utf8'), null, { allowEmpty: true }).metrics);
|
||||
}
|
||||
|
||||
function poiCoordinates(fields, existing = {}) {
|
||||
|
||||
Reference in New Issue
Block a user