Reworked pause and stop buttons and documentation
This commit is contained in:
parent
4529749baa
commit
40f230864b
45
README.md
45
README.md
@ -68,44 +68,9 @@ public/vendor/jquery-ui/images/*.png
|
|||||||
|
|
||||||
Die Anwendung verwendet aus jQuery UI insbesondere die Widgets **Button** und **Controlgroup**. Das offizielle vollständige jQuery-UI-Bundle bleibt lokal verfügbar, damit weitere aktuelle Widgets ohne erneuten CDN-Bezug ergänzt werden können. Die frühere jQuery-Mobile-Seitensteuerung wurde durch eine eigene, History-API-basierte Navigation ersetzt.
|
Die Anwendung verwendet aus jQuery UI insbesondere die Widgets **Button** und **Controlgroup**. Das offizielle vollständige jQuery-UI-Bundle bleibt lokal verfügbar, damit weitere aktuelle Widgets ohne erneuten CDN-Bezug ergänzt werden können. Die frühere jQuery-Mobile-Seitensteuerung wurde durch eine eigene, History-API-basierte Navigation ersetzt.
|
||||||
|
|
||||||
## REST-API
|
## API-Dokumentation
|
||||||
|
|
||||||
| Methode | Pfad | Zweck |
|
Die vollständige REST-API ist einschließlich aller Pfad-, Query-, Formular- und Datei-Parameter sowie ausführlicher `curl`-Beispiele in [`docs/REST-API.md`](docs/REST-API.md) dokumentiert. Die README beschreibt bewusst nur Installation, Architektur und Bedienverhalten.
|
||||||
|---|---|---|
|
|
||||||
| GET | `/api/health` | Server- und SQLite-Selbsttest |
|
|
||||||
| GET | `/api/routes?lat=…&lon=…&radiusKm=25` | aktive, optional nahe Routen |
|
|
||||||
| GET | `/api/routes/:id` | Route, Trackpunkte, POIs und Medien-URLs |
|
|
||||||
| GET | `/api/routes/:id/pois` | POIs einer Route |
|
|
||||||
| GET | `/api/pois/:id` | einzelner POI samt Bild- und Audio-URLs |
|
|
||||||
| POST | `/api/routes` | neue Route; `multipart/form-data`, Feld `gpx` |
|
|
||||||
| PUT | `/api/routes/:id` | Metadaten und optional GPX vollständig ersetzen |
|
|
||||||
| POST | `/api/routes/:id/append` | zusätzliche GPX-Trackpunkte anhängen |
|
|
||||||
| POST | `/api/routes/:id/pois` | POI anlegen; Felder `audio` und `images` |
|
|
||||||
| PUT | `/api/pois/:id` | POI ändern und weitere Medien ergänzen |
|
|
||||||
| DELETE | `/api/routes/:id` | Route samt Medien in den Papierkorb verschieben |
|
|
||||||
| POST | `/api/routes/:id/restore` | Route aus dem Papierkorb wiederherstellen |
|
|
||||||
|
|
||||||
### Route anlegen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://127.0.0.1:47145/api/routes \
|
|
||||||
-F 'name=Schulwald-Runde' \
|
|
||||||
-F 'schoolName=Beispielschule' \
|
|
||||||
-F 'description=Naturkundlicher Rundweg' \
|
|
||||||
-F 'gpx=@examples/sample-route.gpx;type=application/gpx+xml'
|
|
||||||
```
|
|
||||||
|
|
||||||
### POI mit Bildern und Audio anlegen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://127.0.0.1:47145/api/routes/1/pois \
|
|
||||||
-F 'title=Die alte Eiche' \
|
|
||||||
-F 'description=Hier wird das Alter der Eiche erklärt.' \
|
|
||||||
-F 'lat=52.5208' -F 'lon=13.4070' -F 'triggerRadiusM=60' \
|
|
||||||
-F 'audio=@ansage.mp3;type=audio/mpeg' \
|
|
||||||
-F 'images=@eiche-1.jpg;type=image/jpeg' \
|
|
||||||
-F 'images=@eiche-2.jpg;type=image/jpeg'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dateistruktur
|
## Dateistruktur
|
||||||
|
|
||||||
@ -171,8 +136,10 @@ Beim Start der Route wird der aktuellen Position nächstgelegene GPX-Trackpunkt
|
|||||||
Die Routenansicht besitzt drei nebeneinanderliegende, semantische Schaltflächen mit lokalen SVG-Symbolen:
|
Die Routenansicht besitzt drei nebeneinanderliegende, semantische Schaltflächen mit lokalen SVG-Symbolen:
|
||||||
|
|
||||||
- **Route starten beziehungsweise fortsetzen:** aktiviert `navigator.geolocation.watchPosition()` und die Geräteausrichtung.
|
- **Route starten beziehungsweise fortsetzen:** aktiviert `navigator.geolocation.watchPosition()` und die Geräteausrichtung.
|
||||||
- **Route pausieren:** beendet den aktiven GPS-Watcher und den Kompass-Listener, behält aber Routenfortschritt und bereits ausgelöste POIs bei.
|
- **Route pausieren:** beendet den aktiven GPS-Watcher und den Kompass-Listener, pausiert eine laufende Audioansage und behält Routenfortschritt, Audioquelle, Abspielposition und bereits ausgelöste POIs bei.
|
||||||
- **Route beenden:** beendet alle Sensor-Listener, setzt den Fortschritt zurück und leert die Liste bereits ausgelöster POIs.
|
- **Route beenden:** beendet alle Sensor-Listener, hält die Audioansage an, entlädt ihre Quelldatei, setzt den Fortschritt zurück und kehrt zur Routenauswahl zurück.
|
||||||
|
|
||||||
|
Die aktuell geöffnete beziehungsweise automatisch ausgelöste Station wird in der Stationsliste sichtbar hervorgehoben und mit `aria-current="step"` semantisch gekennzeichnet.
|
||||||
|
|
||||||
Während die Route läuft, werden Positionsänderungen fortlaufend verarbeitet. Eine Browser-Webanwendung ist jedoch kein nativer Hintergrunddienst: Betriebssystem und Browser können die Aktualisierung bei gesperrtem Bildschirm, Energiesparmodus oder im Hintergrund drosseln beziehungsweise anhalten.
|
Während die Route läuft, werden Positionsänderungen fortlaufend verarbeitet. Eine Browser-Webanwendung ist jedoch kein nativer Hintergrunddienst: Betriebssystem und Browser können die Aktualisierung bei gesperrtem Bildschirm, Energiesparmodus oder im Hintergrund drosseln beziehungsweise anhalten.
|
||||||
|
|
||||||
|
|||||||
636
docs/REST-API.md
Normal file
636
docs/REST-API.md
Normal file
@ -0,0 +1,636 @@
|
|||||||
|
# Wegwichtel REST-API
|
||||||
|
|
||||||
|
Diese Datei dokumentiert die vollständige HTTP-Schnittstelle des aktuellen Wegwichtel-Servers. Die API wird unter `/api` bereitgestellt. Aktive GPX-, Bild- und Audiodateien werden zusätzlich unter `/media` ausgeliefert.
|
||||||
|
|
||||||
|
## 1. Grundlagen
|
||||||
|
|
||||||
|
### Basisadresse
|
||||||
|
|
||||||
|
Lokaler Standard:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:47145
|
||||||
|
```
|
||||||
|
|
||||||
|
API-Basis:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:47145/api
|
||||||
|
```
|
||||||
|
|
||||||
|
Bei einer Nginx-Installation bleibt der Pfad gleich, beispielsweise:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://wegwichtel.example.org/api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Formate
|
||||||
|
|
||||||
|
- Lesezugriffe liefern JSON.
|
||||||
|
- Routen- und POI-Schreibzugriffe mit Dateien verwenden `multipart/form-data`.
|
||||||
|
- Fehler werden als JSON ausgegeben.
|
||||||
|
- Zeitstempel stammen aus SQLite beziehungsweise JavaScript und werden als Text oder ISO-8601-Zeitstempel ausgegeben.
|
||||||
|
- Die aktuelle API besitzt noch keine Authentifizierung. Schreibzugriffe dürfen deshalb nicht ungeschützt öffentlich erreichbar sein.
|
||||||
|
|
||||||
|
### Allgemeines Fehlerformat
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "HttpError",
|
||||||
|
"message": "Strecke nicht gefunden."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Bei Detailinformationen kann zusätzlich `details` enthalten sein:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "HttpError",
|
||||||
|
"message": "Die GPX-Datei ist kein gültiges XML.",
|
||||||
|
"details": "Parsermeldung"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Typische Statuscodes:
|
||||||
|
|
||||||
|
| Status | Bedeutung |
|
||||||
|
|---:|---|
|
||||||
|
| `200` | Anfrage erfolgreich |
|
||||||
|
| `201` | Ressource wurde angelegt |
|
||||||
|
| `400` | Parameter oder Upload unvollständig beziehungsweise ungültig |
|
||||||
|
| `404` | Route, POI oder Datei wurde nicht gefunden |
|
||||||
|
| `409` | Dateisystemzustand verhindert Löschen oder Wiederherstellen |
|
||||||
|
| `413` | Eine hochgeladene Datei überschreitet das konfigurierte Limit |
|
||||||
|
| `415` | Dateityp wird nicht unterstützt |
|
||||||
|
| `500` | Interner Serverfehler |
|
||||||
|
|
||||||
|
### IDs
|
||||||
|
|
||||||
|
Die Pfadparameter `:id` sind positive, von SQLite erzeugte Ganzzahlen. Beispiele verwenden überwiegend Route `1` und POI `7`.
|
||||||
|
|
||||||
|
### Uploadgrenzen und Dateitypen
|
||||||
|
|
||||||
|
Das Dateilimit pro Datei wird durch `MAX_UPLOAD_MB` festgelegt und beträgt standardmäßig `50 MB`. Multer akzeptiert höchstens 25 Dateien pro Request.
|
||||||
|
|
||||||
|
Erlaubte Uploadtypen:
|
||||||
|
|
||||||
|
- GPX/XML: `.gpx`, `application/gpx+xml`, `application/xml`, `text/xml`
|
||||||
|
- Bilder: JPEG, PNG, WebP
|
||||||
|
- Audio: MP3, MP4/M4A, AAC, Ogg, WAV, WebM
|
||||||
|
|
||||||
|
Beim Anlegen oder Aktualisieren eines POIs gelten zusätzlich:
|
||||||
|
|
||||||
|
- höchstens eine Datei im Feld `audio`
|
||||||
|
- höchstens 20 Dateien im Feld `images`
|
||||||
|
|
||||||
|
## 2. Datenmodelle
|
||||||
|
|
||||||
|
### Kurzfassung einer Route
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"slug": "schulwald-runde",
|
||||||
|
"name": "Schulwald-Runde",
|
||||||
|
"description": "Naturkundlicher Rundweg",
|
||||||
|
"schoolName": "Beispielschule",
|
||||||
|
"status": "active",
|
||||||
|
"start": { "lat": 52.5208, "lon": 13.4070 },
|
||||||
|
"center": { "lat": 52.5210, "lon": 13.4080 },
|
||||||
|
"bounds": {
|
||||||
|
"minLat": 52.5208,
|
||||||
|
"minLon": 13.4070,
|
||||||
|
"maxLat": 52.5212,
|
||||||
|
"maxLon": 13.4090
|
||||||
|
},
|
||||||
|
"distanceM": 842.6,
|
||||||
|
"elevationGainM": 14.2,
|
||||||
|
"pointCount": 87,
|
||||||
|
"gpxUrl": "/media/routes/1/route.gpx",
|
||||||
|
"createdAt": "2026-06-16 12:00:00",
|
||||||
|
"updatedAt": "2026-06-16 12:00:00",
|
||||||
|
"deletedAt": null,
|
||||||
|
"proximityM": 324.8
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`proximityM` ist `null`, wenn beim Listenaufruf keine gültige Position übergeben wurde. Bei gelöschten Routen ist `gpxUrl` `null`, weil Dateien im Papierkorb nicht öffentlich ausgeliefert werden.
|
||||||
|
|
||||||
|
### Vollständige Route
|
||||||
|
|
||||||
|
`GET /api/routes/:id` ergänzt die Kurzfassung um `points` und `pois`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Schulwald-Runde",
|
||||||
|
"points": [
|
||||||
|
{
|
||||||
|
"sequence": 0,
|
||||||
|
"lat": 52.5208,
|
||||||
|
"lon": 13.4070,
|
||||||
|
"elevation": 71.4,
|
||||||
|
"recordedAt": "2026-06-16T09:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pois": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POI
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"routeId": 1,
|
||||||
|
"title": "Die alte Eiche",
|
||||||
|
"description": "Hier wird das Alter der Eiche erklärt.",
|
||||||
|
"lat": 52.5208,
|
||||||
|
"lon": 13.4070,
|
||||||
|
"triggerRadiusM": 60,
|
||||||
|
"sequence": 2,
|
||||||
|
"audioUrl": "/media/routes/1/audio/uuid.mp3",
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"caption": "",
|
||||||
|
"sequence": 0,
|
||||||
|
"url": "/media/routes/1/images/uuid.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Systemzustand
|
||||||
|
|
||||||
|
### `GET /api/health`
|
||||||
|
|
||||||
|
Prüft den Node.js-Prozess und eine SQLite-Abfrage.
|
||||||
|
|
||||||
|
Parameter: keine.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:47145/api/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Beispielantwort:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"service": "wegwichtel",
|
||||||
|
"socket": "127.0.0.1:47145",
|
||||||
|
"sqliteVersion": "3.46.1",
|
||||||
|
"timestamp": "2026-06-16T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Routen lesen
|
||||||
|
|
||||||
|
### `GET /api/routes`
|
||||||
|
|
||||||
|
Liefert standardmäßig alle aktiven Routen alphabetisch. Werden `lat` und `lon` gemeinsam übergeben, berechnet der Server die Entfernung zum Startpunkt, filtert anhand `radiusKm` und sortiert nach Entfernung.
|
||||||
|
|
||||||
|
#### Query-Parameter
|
||||||
|
|
||||||
|
| Parameter | Typ | Pflicht | Standard | Beschreibung |
|
||||||
|
|---|---|---:|---:|---|
|
||||||
|
| `lat` | Dezimalzahl | nein | – | Breitengrad der aktuellen Position; nur zusammen mit `lon` wirksam |
|
||||||
|
| `lon` | Dezimalzahl | nein | – | Längengrad der aktuellen Position; nur zusammen mit `lat` wirksam |
|
||||||
|
| `radiusKm` | Dezimalzahl | nein | `DEFAULT_ROUTE_RADIUS_KM`, standardmäßig `25` | maximaler Abstand zum Routenstart; nur bei gültigem `lat` und `lon` wirksam |
|
||||||
|
| `includeDeleted` | Boolean-Text | nein | `false` | nur der exakte Wert `true` schließt gelöschte Routen ein |
|
||||||
|
|
||||||
|
Alle aktiven Routen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:47145/api/routes
|
||||||
|
```
|
||||||
|
|
||||||
|
Alle Parameter in einem Beispiel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --get http://127.0.0.1:47145/api/routes \
|
||||||
|
--data-urlencode 'lat=52.5208' \
|
||||||
|
--data-urlencode 'lon=13.4070' \
|
||||||
|
--data-urlencode 'radiusKm=12.5' \
|
||||||
|
--data-urlencode 'includeDeleted=true'
|
||||||
|
```
|
||||||
|
|
||||||
|
Antwort:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Schulwald-Runde",
|
||||||
|
"status": "active",
|
||||||
|
"proximityM": 324.8
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Hinweise:
|
||||||
|
|
||||||
|
- Eine ungültige Zahl wird intern wie ein nicht gesetzter Wert behandelt.
|
||||||
|
- Der Positionsfilter wird nur aktiv, wenn sowohl `lat` als auch `lon` gültige Zahlen sind.
|
||||||
|
- `radiusKm` wird derzeit nicht auf einen Mindest- oder Höchstwert begrenzt.
|
||||||
|
|
||||||
|
### `GET /api/routes/:id`
|
||||||
|
|
||||||
|
Liefert eine Route mit sämtlichen GPX-Punkten und POIs.
|
||||||
|
|
||||||
|
#### Pfadparameter
|
||||||
|
|
||||||
|
| Parameter | Typ | Pflicht | Beschreibung |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| `id` | Ganzzahl | ja | ID der Route |
|
||||||
|
|
||||||
|
#### Query-Parameter
|
||||||
|
|
||||||
|
| Parameter | Typ | Pflicht | Standard | Beschreibung |
|
||||||
|
|---|---|---:|---:|---|
|
||||||
|
| `includeDeleted` | Boolean-Text | nein | `false` | mit `true` kann auch eine als gelöscht markierte Route gelesen werden |
|
||||||
|
|
||||||
|
Aktive Route:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:47145/api/routes/1
|
||||||
|
```
|
||||||
|
|
||||||
|
Gelöschte Route ausdrücklich einschließen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl 'http://127.0.0.1:47145/api/routes/1?includeDeleted=true'
|
||||||
|
```
|
||||||
|
|
||||||
|
Bei einer gelöschten Route sind Medien-URLs `null`, weil der Papierkorb nicht unter `/media` veröffentlicht wird.
|
||||||
|
|
||||||
|
### `GET /api/routes/:id/pois`
|
||||||
|
|
||||||
|
Liefert alle POIs einer aktiven Route in der Reihenfolge `sequence`, anschließend `id`.
|
||||||
|
|
||||||
|
#### Pfadparameter
|
||||||
|
|
||||||
|
| Parameter | Typ | Pflicht | Beschreibung |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| `id` | Ganzzahl | ja | ID der aktiven Route |
|
||||||
|
|
||||||
|
Weitere Parameter: keine.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:47145/api/routes/1/pois
|
||||||
|
```
|
||||||
|
|
||||||
|
Antwort:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pois": [
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"routeId": 1,
|
||||||
|
"title": "Die alte Eiche",
|
||||||
|
"sequence": 2,
|
||||||
|
"audioUrl": "/media/routes/1/audio/uuid.mp3",
|
||||||
|
"images": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. POIs lesen
|
||||||
|
|
||||||
|
### `GET /api/pois/:id`
|
||||||
|
|
||||||
|
Liefert einen einzelnen POI einschließlich Bild- und Audio-URLs. Der POI muss zu einer aktiven Route gehören.
|
||||||
|
|
||||||
|
#### Pfadparameter
|
||||||
|
|
||||||
|
| Parameter | Typ | Pflicht | Beschreibung |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| `id` | Ganzzahl | ja | ID des POIs |
|
||||||
|
|
||||||
|
Weitere Parameter: keine.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:47145/api/pois/7
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Route anlegen
|
||||||
|
|
||||||
|
### `POST /api/routes`
|
||||||
|
|
||||||
|
Legt eine neue Route aus einer GPX-Datei an. Trackpunkte, Streckenlänge, Höhengewinn, Startpunkt, Mittelpunkt und Grenzen werden aus der Datei berechnet.
|
||||||
|
|
||||||
|
Content-Type: `multipart/form-data`
|
||||||
|
|
||||||
|
#### Formular- und Dateiparameter
|
||||||
|
|
||||||
|
| Feld | Typ | Pflicht | Standard | Beschreibung |
|
||||||
|
|---|---|---:|---|---|
|
||||||
|
| `gpx` | Datei | ja | – | GPX-Datei mit mindestens einem verwertbaren `trkpt` |
|
||||||
|
| `name` | Text | bedingt | GPX-Track- oder Metadatenname | Routenname; erforderlich, falls die GPX-Datei keinen Namen enthält |
|
||||||
|
| `slug` | Text | nein | aus `name` abgeleitet | URL-freundliche interne Kennung; Kollisionen erhalten automatisch `-2`, `-3` usw. |
|
||||||
|
| `description` | Text | nein | leer | Beschreibung der Route |
|
||||||
|
| `schoolName` | Text | nein | leer | Name der Schule oder Einrichtung |
|
||||||
|
|
||||||
|
Beispiel mit allen Parametern:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://127.0.0.1:47145/api/routes \
|
||||||
|
-F 'name=Schulwald-Runde' \
|
||||||
|
-F 'slug=schulwald-runde-klasse-7a' \
|
||||||
|
-F 'description=Naturkundlicher Rundweg der Klasse 7a' \
|
||||||
|
-F 'schoolName=Beispielschule' \
|
||||||
|
-F 'gpx=@examples/sample-route.gpx;type=application/gpx+xml'
|
||||||
|
```
|
||||||
|
|
||||||
|
Erfolg:
|
||||||
|
|
||||||
|
- Status `201 Created`
|
||||||
|
- Header `Location: /api/routes/<id>`
|
||||||
|
- Body: vollständige neu angelegte Route
|
||||||
|
|
||||||
|
## 7. Route vollständig aktualisieren
|
||||||
|
|
||||||
|
### `PUT /api/routes/:id`
|
||||||
|
|
||||||
|
Ändert Metadaten einer aktiven Route. Eine optionale GPX-Datei ersetzt alle bisherigen Trackpunkte und die Datei `route.gpx`.
|
||||||
|
|
||||||
|
Content-Type: `multipart/form-data`
|
||||||
|
|
||||||
|
#### Pfadparameter
|
||||||
|
|
||||||
|
| Parameter | Typ | Pflicht | Beschreibung |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| `id` | Ganzzahl | ja | ID der aktiven Route |
|
||||||
|
|
||||||
|
#### Formular- und Dateiparameter
|
||||||
|
|
||||||
|
| Feld | Typ | Pflicht | Verhalten bei Auslassung | Beschreibung |
|
||||||
|
|---|---|---:|---|---|
|
||||||
|
| `name` | Text | nein | alter Wert bleibt | neuer Routenname |
|
||||||
|
| `description` | Text | nein | alter Wert bleibt | neue Beschreibung; leere Zeichenfolge löscht den Inhalt |
|
||||||
|
| `schoolName` | Text | nein | alter Wert bleibt | neuer Schulname; leere Zeichenfolge löscht den Inhalt |
|
||||||
|
| `gpx` | Datei | nein | Track bleibt unverändert | ersetzt GPX-Datei, Trackpunkte und berechnete Kennzahlen vollständig |
|
||||||
|
|
||||||
|
`slug` kann über diesen Endpunkt derzeit nicht geändert werden.
|
||||||
|
|
||||||
|
Beispiel mit allen Parametern:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PUT http://127.0.0.1:47145/api/routes/1 \
|
||||||
|
-F 'name=Schulwald-Runde – überarbeitet' \
|
||||||
|
-F 'description=Neue Wegführung ab dem Schulhof' \
|
||||||
|
-F 'schoolName=Beispielschule Nord' \
|
||||||
|
-F 'gpx=@examples/sample-route.gpx;type=application/gpx+xml'
|
||||||
|
```
|
||||||
|
|
||||||
|
Nur die Beschreibung ändern:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PUT http://127.0.0.1:47145/api/routes/1 \
|
||||||
|
-F 'description=Nur dieser Wert wird geändert.'
|
||||||
|
```
|
||||||
|
|
||||||
|
Antwort: vollständige aktualisierte Route.
|
||||||
|
|
||||||
|
## 8. GPX-Punkte an eine Route anhängen
|
||||||
|
|
||||||
|
### `POST /api/routes/:id/append`
|
||||||
|
|
||||||
|
Hängt sämtliche verwertbaren Trackpunkte einer GPX-Datei an eine aktive Route an und berechnet die Streckenkennzahlen neu.
|
||||||
|
|
||||||
|
Content-Type: `multipart/form-data`
|
||||||
|
|
||||||
|
#### Pfadparameter
|
||||||
|
|
||||||
|
| Parameter | Typ | Pflicht | Beschreibung |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| `id` | Ganzzahl | ja | ID der aktiven Route |
|
||||||
|
|
||||||
|
#### Dateiparameter
|
||||||
|
|
||||||
|
| Feld | Typ | Pflicht | Beschreibung |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| `gpx` | Datei | ja | GPX-Datei mit den anzuhängenden Trackpunkten |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://127.0.0.1:47145/api/routes/1/append \
|
||||||
|
-F 'gpx=@weiterer-abschnitt.gpx;type=application/gpx+xml'
|
||||||
|
```
|
||||||
|
|
||||||
|
Hinweis: Die Punkte werden in SQLite ergänzt. Die gespeicherte Originaldatei `route.gpx` wird durch diesen Endpunkt derzeit nicht zu einer konsolidierten GPX-Datei erweitert.
|
||||||
|
|
||||||
|
## 9. POI anlegen
|
||||||
|
|
||||||
|
### `POST /api/routes/:id/pois`
|
||||||
|
|
||||||
|
Legt einen POI für eine aktive Route an und speichert optional eine Audioansage und mehrere Bilder.
|
||||||
|
|
||||||
|
Content-Type: `multipart/form-data`
|
||||||
|
|
||||||
|
#### Pfadparameter
|
||||||
|
|
||||||
|
| Parameter | Typ | Pflicht | Beschreibung |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| `id` | Ganzzahl | ja | ID der aktiven Route |
|
||||||
|
|
||||||
|
#### Formular- und Dateiparameter
|
||||||
|
|
||||||
|
| Feld | Typ | Pflicht | Standard | Beschreibung |
|
||||||
|
|---|---|---:|---|---|
|
||||||
|
| `title` | Text | nein | `Unbenannter POI` | Titel der Station |
|
||||||
|
| `description` | Text | nein | leer | Beschreibung der Station |
|
||||||
|
| `lat` | Dezimalzahl | ja | – | Breitengrad des POIs |
|
||||||
|
| `lon` | Dezimalzahl | ja | – | Längengrad des POIs |
|
||||||
|
| `triggerRadiusM` | Dezimalzahl | nein | `DEFAULT_POI_TRIGGER_METERS`, standardmäßig `80` | Entfernung in Metern, ab der die Station automatisch ausgelöst wird |
|
||||||
|
| `sequence` | Ganzzahl | nein | `0` | Sortierreihenfolge innerhalb der Route |
|
||||||
|
| `audio` | Datei | nein | keine | eine Audioansage |
|
||||||
|
| `images` | Datei, wiederholbar | nein | keine | bis zu 20 Bilder; jedes Bild wird als eigenes Feld `images` gesendet |
|
||||||
|
|
||||||
|
Beispiel mit allen Parametern:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://127.0.0.1:47145/api/routes/1/pois \
|
||||||
|
-F 'title=Die alte Eiche' \
|
||||||
|
-F 'description=Hier wird das Alter der Eiche erklärt.' \
|
||||||
|
-F 'lat=52.5208' \
|
||||||
|
-F 'lon=13.4070' \
|
||||||
|
-F 'triggerRadiusM=60' \
|
||||||
|
-F 'sequence=2' \
|
||||||
|
-F 'audio=@ansage.mp3;type=audio/mpeg' \
|
||||||
|
-F 'images=@eiche-1.jpg;type=image/jpeg' \
|
||||||
|
-F 'images=@eiche-2.webp;type=image/webp'
|
||||||
|
```
|
||||||
|
|
||||||
|
Erfolg:
|
||||||
|
|
||||||
|
- Status `201 Created`
|
||||||
|
- Header `Location: /api/pois/<id>`
|
||||||
|
- Body: neu angelegter POI
|
||||||
|
|
||||||
|
Bildunterschriften können in der aktuellen API noch nicht per Parameter gesetzt werden und bleiben leer.
|
||||||
|
|
||||||
|
## 10. POI aktualisieren und Medien ergänzen
|
||||||
|
|
||||||
|
### `PUT /api/pois/:id`
|
||||||
|
|
||||||
|
Ändert einen POI einer aktiven Route. Eine neue Audiodatei ersetzt den in der Datenbank referenzierten Audiopfad. Neue Bilder werden an die vorhandene Bilderliste angehängt.
|
||||||
|
|
||||||
|
Content-Type: `multipart/form-data`
|
||||||
|
|
||||||
|
#### Pfadparameter
|
||||||
|
|
||||||
|
| Parameter | Typ | Pflicht | Beschreibung |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| `id` | Ganzzahl | ja | ID des POIs |
|
||||||
|
|
||||||
|
#### Formular- und Dateiparameter
|
||||||
|
|
||||||
|
| Feld | Typ | Pflicht | Verhalten bei Auslassung | Beschreibung |
|
||||||
|
|---|---|---:|---|---|
|
||||||
|
| `title` | Text | nein | alter Wert bleibt | neuer Titel |
|
||||||
|
| `description` | Text | nein | alter Wert bleibt | neue Beschreibung |
|
||||||
|
| `lat` | Dezimalzahl | nein | alter Wert bleibt | neuer Breitengrad |
|
||||||
|
| `lon` | Dezimalzahl | nein | alter Wert bleibt | neuer Längengrad |
|
||||||
|
| `triggerRadiusM` | Dezimalzahl | nein | alter Wert bleibt | neuer automatischer Auslöseradius in Metern |
|
||||||
|
| `sequence` | Ganzzahl | nein | alter Wert bleibt | neue Sortierreihenfolge |
|
||||||
|
| `audio` | Datei | nein | alte Referenz bleibt | neue Audioansage; maximal eine Datei |
|
||||||
|
| `images` | Datei, wiederholbar | nein | Bilder bleiben unverändert | bis zu 20 zusätzliche Bilder |
|
||||||
|
|
||||||
|
Beispiel mit allen Parametern:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PUT http://127.0.0.1:47145/api/pois/7 \
|
||||||
|
-F 'title=Die sehr alte Eiche' \
|
||||||
|
-F 'description=Überarbeitete Ansage und neue Fotos.' \
|
||||||
|
-F 'lat=52.5209' \
|
||||||
|
-F 'lon=13.4072' \
|
||||||
|
-F 'triggerRadiusM=45' \
|
||||||
|
-F 'sequence=3' \
|
||||||
|
-F 'audio=@ansage-neu.ogg;type=audio/ogg' \
|
||||||
|
-F 'images=@eiche-3.png;type=image/png' \
|
||||||
|
-F 'images=@eiche-4.jpg;type=image/jpeg'
|
||||||
|
```
|
||||||
|
|
||||||
|
Wichtige aktuelle Einschränkungen:
|
||||||
|
|
||||||
|
- Einzelne Bilder können noch nicht per API gelöscht, umsortiert oder beschriftet werden.
|
||||||
|
- Neu hochgeladene Bilder werden hinter vorhandenen Bildern einsortiert.
|
||||||
|
- Beim Ersetzen der Audio-Referenz wird die vorherige Audiodatei derzeit nicht automatisch aus dem aktiven Verzeichnis entfernt.
|
||||||
|
|
||||||
|
## 11. Route zum Löschen markieren
|
||||||
|
|
||||||
|
### `DELETE /api/routes/:id`
|
||||||
|
|
||||||
|
Markiert eine aktive Route als gelöscht und verschiebt ihr gesamtes Verzeichnis einschließlich GPX, Bildern und Audio nach `storage/trash/routes`. Die Datenbankzeilen bleiben erhalten; alle gespeicherten Pfade werden auf den Papierkorb umgeschrieben.
|
||||||
|
|
||||||
|
#### Pfadparameter
|
||||||
|
|
||||||
|
| Parameter | Typ | Pflicht | Beschreibung |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| `id` | Ganzzahl | ja | ID der aktiven Route |
|
||||||
|
|
||||||
|
Weitere Parameter: keine.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X DELETE http://127.0.0.1:47145/api/routes/1
|
||||||
|
```
|
||||||
|
|
||||||
|
Antwort:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"route": {
|
||||||
|
"id": 1,
|
||||||
|
"status": "deleted",
|
||||||
|
"gpxUrl": null,
|
||||||
|
"deletedAt": "2026-06-16 12:30:00"
|
||||||
|
},
|
||||||
|
"softDeleted": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Es werden keine Dateien endgültig entfernt.
|
||||||
|
|
||||||
|
## 12. Route wiederherstellen
|
||||||
|
|
||||||
|
### `POST /api/routes/:id/restore`
|
||||||
|
|
||||||
|
Stellt eine als gelöscht markierte Route wieder her, verschiebt ihr Verzeichnis nach `storage/active/routes/<id>` zurück und korrigiert alle GPX-, Bild- und Audiopfade.
|
||||||
|
|
||||||
|
#### Pfadparameter
|
||||||
|
|
||||||
|
| Parameter | Typ | Pflicht | Beschreibung |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| `id` | Ganzzahl | ja | ID der gelöschten Route |
|
||||||
|
|
||||||
|
Weitere Parameter: keine. Der Request besitzt keinen Body.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://127.0.0.1:47145/api/routes/1/restore
|
||||||
|
```
|
||||||
|
|
||||||
|
Antwort:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"route": {
|
||||||
|
"id": 1,
|
||||||
|
"status": "active",
|
||||||
|
"gpxUrl": "/media/routes/1/route.gpx",
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
"restored": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 13. Medien abrufen
|
||||||
|
|
||||||
|
Aktive Dateien werden nicht unter `/api`, sondern statisch unter `/media` ausgeliefert. Die benötigten URLs stehen in `gpxUrl`, `audioUrl` und `images[].url`.
|
||||||
|
|
||||||
|
Parameter: keine zusätzlichen Query- oder Formularparameter. Der komplette Pfad stammt aus der jeweiligen API-Antwort.
|
||||||
|
|
||||||
|
GPX-Datei:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --output route.gpx \
|
||||||
|
http://127.0.0.1:47145/media/routes/1/route.gpx
|
||||||
|
```
|
||||||
|
|
||||||
|
Audio:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --output ansage.mp3 \
|
||||||
|
http://127.0.0.1:47145/media/routes/1/audio/DATEINAME.mp3
|
||||||
|
```
|
||||||
|
|
||||||
|
Bild:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --output station.jpg \
|
||||||
|
http://127.0.0.1:47145/media/routes/1/images/DATEINAME.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Nur `storage/active` wird veröffentlicht. Dateien gelöschter Routen unter `storage/trash` sind nicht über HTTP erreichbar.
|
||||||
|
|
||||||
|
## 14. Endpunktübersicht
|
||||||
|
|
||||||
|
| Methode | Pfad | Zweck |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET` | `/api/health` | Server und SQLite prüfen |
|
||||||
|
| `GET` | `/api/routes` | Routen auflisten und optional räumlich filtern |
|
||||||
|
| `GET` | `/api/routes/:id` | vollständige Route lesen |
|
||||||
|
| `GET` | `/api/routes/:id/pois` | POIs einer Route lesen |
|
||||||
|
| `GET` | `/api/pois/:id` | einzelnen POI lesen |
|
||||||
|
| `POST` | `/api/routes` | Route aus GPX anlegen |
|
||||||
|
| `PUT` | `/api/routes/:id` | Route aktualisieren oder GPX ersetzen |
|
||||||
|
| `POST` | `/api/routes/:id/append` | GPX-Punkte anhängen |
|
||||||
|
| `POST` | `/api/routes/:id/pois` | POI und Medien anlegen |
|
||||||
|
| `PUT` | `/api/pois/:id` | POI aktualisieren und Medien ergänzen |
|
||||||
|
| `DELETE` | `/api/routes/:id` | Route in den Papierkorb verschieben |
|
||||||
|
| `POST` | `/api/routes/:id/restore` | Route wiederherstellen |
|
||||||
|
| `GET` | `/media/routes/...` | aktive GPX-, Bild- und Audiodateien abrufen |
|
||||||
@ -219,6 +219,19 @@ button,
|
|||||||
opacity: .65;
|
opacity: .65;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#poi-list button[aria-current="step"] {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: #e7f1e9;
|
||||||
|
box-shadow: inset .35rem 0 0 var(--primary), 0 .12rem .35rem rgba(0, 0, 0, .08);
|
||||||
|
}
|
||||||
|
|
||||||
|
#poi-list button[aria-current="step"] > strong::after {
|
||||||
|
content: " · aktuelle Station";
|
||||||
|
color: var(--primary-dark);
|
||||||
|
font-size: .85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.route-list > li > p {
|
.route-list > li > p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|||||||
@ -118,7 +118,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p id="poi-description"></p>
|
<p id="poi-description"></p>
|
||||||
<audio id="poi-audio" controls preload="metadata"></audio>
|
<audio id="poi-audio" controls preload="metadata" hidden></audio>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
routeState: 'idle',
|
routeState: 'idle',
|
||||||
activePage: null,
|
activePage: null,
|
||||||
routeProgressIndex: null,
|
routeProgressIndex: null,
|
||||||
|
activePoiId: null,
|
||||||
deviceHeading: null
|
deviceHeading: null
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -169,21 +170,36 @@
|
|||||||
$('#stop-route').prop('disabled', state.routeState === 'idle');
|
$('#stop-route').prop('disabled', state.routeState === 'idle');
|
||||||
}
|
}
|
||||||
|
|
||||||
function endRoute({ announce = true } = {}) {
|
function setActivePoi(poiId) {
|
||||||
|
state.activePoiId = poiId == null ? null : Number(poiId);
|
||||||
|
$('#poi-list [data-poi-id]').removeAttr('aria-current');
|
||||||
|
if (state.activePoiId != null) {
|
||||||
|
$(`#poi-list [data-poi-id="${state.activePoiId}"]`).attr('aria-current', 'step');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function endRoute({ announce = true, returnToRoutes = false } = {}) {
|
||||||
const wasActive = state.routeState !== 'idle';
|
const wasActive = state.routeState !== 'idle';
|
||||||
state.routeState = 'idle';
|
state.routeState = 'idle';
|
||||||
state.routeProgressIndex = null;
|
state.routeProgressIndex = null;
|
||||||
state.triggered.clear();
|
state.triggered.clear();
|
||||||
stopRouteSensors();
|
stopRouteSensors();
|
||||||
|
ns.AudioPlayer.unload();
|
||||||
|
setActivePoi(null);
|
||||||
hideNavigation();
|
hideNavigation();
|
||||||
|
|
||||||
if (announce && wasActive) {
|
if (announce && wasActive && !returnToRoutes) {
|
||||||
$('#tracking-status').prop('hidden', false).text('Route beendet.');
|
$('#tracking-status').prop('hidden', false).text('Route beendet.');
|
||||||
} else {
|
} else {
|
||||||
$('#tracking-status').prop('hidden', true).text('');
|
$('#tracking-status').prop('hidden', true).text('');
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRouteControls();
|
updateRouteControls();
|
||||||
|
|
||||||
|
if (returnToRoutes) {
|
||||||
|
state.route = null;
|
||||||
|
navigate('#routes-page', { replace: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pauseRoute() {
|
function pauseRoute() {
|
||||||
@ -191,6 +207,7 @@
|
|||||||
|
|
||||||
state.routeState = 'paused';
|
state.routeState = 'paused';
|
||||||
stopRouteSensors();
|
stopRouteSensors();
|
||||||
|
ns.AudioPlayer.pause();
|
||||||
$('#tracking-status')
|
$('#tracking-status')
|
||||||
.prop('hidden', false)
|
.prop('hidden', false)
|
||||||
.text('Route pausiert. Die Standortverfolgung ist angehalten.');
|
.text('Route pausiert. Die Standortverfolgung ist angehalten.');
|
||||||
@ -204,6 +221,7 @@
|
|||||||
state.route = route;
|
state.route = route;
|
||||||
state.triggered.clear();
|
state.triggered.clear();
|
||||||
state.routeProgressIndex = null;
|
state.routeProgressIndex = null;
|
||||||
|
setActivePoi(null);
|
||||||
$('#route-title').text(route.name);
|
$('#route-title').text(route.name);
|
||||||
$('#route-description').text(route.description || '');
|
$('#route-description').text(route.description || '');
|
||||||
$('#route-distance').text(ns.Distance.format(route.distanceM));
|
$('#route-distance').text(ns.Distance.format(route.distanceM));
|
||||||
@ -231,6 +249,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showPoi(poi, automatic) {
|
function showPoi(poi, automatic) {
|
||||||
|
setActivePoi(poi.id);
|
||||||
$('#poi-title').text(poi.title);
|
$('#poi-title').text(poi.title);
|
||||||
$('#poi-description').text(poi.description || '');
|
$('#poi-description').text(poi.description || '');
|
||||||
ns.Slideshow.show(poi.images);
|
ns.Slideshow.show(poi.images);
|
||||||
@ -425,7 +444,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
activate('#stop-route', function () {
|
activate('#stop-route', function () {
|
||||||
endRoute();
|
endRoute({ returnToRoutes: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('popstate', event => {
|
window.addEventListener('popstate', event => {
|
||||||
|
|||||||
@ -1,11 +1,39 @@
|
|||||||
(function (ns, $) {
|
(function (ns, $) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
function element() {
|
||||||
|
return $('#poi-audio')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function unload() {
|
||||||
|
const audio = element();
|
||||||
|
audio.pause();
|
||||||
|
audio.removeAttribute('src');
|
||||||
|
audio.load();
|
||||||
|
$(audio).prop('hidden', true);
|
||||||
|
}
|
||||||
|
|
||||||
ns.AudioPlayer = {
|
ns.AudioPlayer = {
|
||||||
load(url) {
|
load(url) {
|
||||||
const audio = $('#poi-audio');
|
unload();
|
||||||
audio[0].pause(); audio.attr('src', url || '').toggle(Boolean(url));
|
if (!url) return;
|
||||||
if (url) audio[0].load();
|
|
||||||
|
const audio = element();
|
||||||
|
audio.src = url;
|
||||||
|
$(audio).prop('hidden', false);
|
||||||
|
audio.load();
|
||||||
},
|
},
|
||||||
play() { const audio = $('#poi-audio')[0]; if (audio.src) return audio.play(); }
|
|
||||||
|
play() {
|
||||||
|
const audio = element();
|
||||||
|
if (audio.getAttribute('src')) return audio.play();
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
pause() {
|
||||||
|
element().pause();
|
||||||
|
},
|
||||||
|
|
||||||
|
unload
|
||||||
};
|
};
|
||||||
}(window.Wegwichtel, window.jQuery));
|
}(window.Wegwichtel, window.jQuery));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user