initial commit

This commit is contained in:
Florian Zumpe 2026-06-16 10:31:41 +02:00
parent e607876d37
commit eb559b16fb
282 changed files with 3118 additions and 41422 deletions

8
.env.example Normal file
View File

@ -0,0 +1,8 @@
HOST=127.0.0.1
PORT=47145
INSTANCE_NAME=Atlas
DATA_DIR=./data
STORAGE_DIR=./storage
MAX_UPLOAD_MB=50
DEFAULT_ROUTE_RADIUS_KM=25
DEFAULT_POI_TRIGGER_METERS=80

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
node_modules/
data/*.sqlite
data/*.sqlite-*
storage/active/routes/*
storage/trash/routes/*
!storage/active/routes/.gitkeep
!storage/trash/routes/.gitkeep
.env
npm-debug.log*

146
README.md Normal file
View File

@ -0,0 +1,146 @@
# Wegwichtel Next
Neuaufbau des früheren schiffsbezogenen Ansagesystems als mobile Lernweg-Anwendung für Schulen. GPX-Strecken werden serverseitig verwaltet; POIs können Bilder und eine Audioansage enthalten. Der Client schlägt anhand der aktuellen Position nahe Routen vor und aktiviert POIs während einer Wanderung über einen GPS-Watcher.
## Architektur
- **Server:** Node.js 22.13+, Express 5, `node:sqlite`, Multer und `fast-xml-parser`
- **Client:** klassisches JavaScript, jQuery 4.0.0 und jQuery UI 1.14.2 mit lokalem Base-Theme
- **Medien:** getrennte Verzeichnisse für aktive und zum Löschen markierte Strecken
- **Löschmodell:** Soft Delete in SQLite plus atomisches Verschieben des gesamten Streckenordners in `storage/trash/routes`
- **Wiederherstellung:** `POST /api/routes/:id/restore` verschiebt die Daten zurück und korrigiert alle gespeicherten Pfade
## Schnellstart
```bash
cp .env.example .env
npm install
npm run init-db
npm start
```
Danach lauscht die Instanz **Atlas** ausschließlich auf dem lokalen Socket `127.0.0.1:47145` und ist unter `http://127.0.0.1:47145` erreichbar. Die offiziellen, exakt versionierten Distributionsdateien sind bereits unter `public/vendor` enthalten. `npm install` installiert zusätzlich die Pakete `jquery@4.0.0` und `jquery-ui@1.14.2` und synchronisiert daraus JavaScript, Base-Theme, Themebilder und Lizenzdateien erneut in das Vendor-Verzeichnis. Der Browser lädt keine Bibliotheken von einem CDN.
## Server-Socket und Instanz Atlas
Die Standardwerte stehen in `.env.example` und werden auch verwendet, wenn keine `.env` vorhanden ist:
```dotenv
HOST=127.0.0.1
PORT=47145
INSTANCE_NAME=Atlas
```
`HOST` ist die tatsächliche Bind-Adresse des Node.js-Servers. `INSTANCE_NAME` ist eine lesbare Bezeichnung und ändert weder DNS noch die Bind-Adresse. Dadurch verweist die Anwendung auf **Atlas**, bleibt aber auf den lokalen Loopback-Socket beschränkt. Der Health-Endpunkt meldet beispielsweise:
```json
{
"ok": true,
"service": "wegwichtel",
"instance": "Atlas",
"socket": "127.0.0.1:47145"
}
```
## Initialisierungsablauf des Clients
1. statischer Initialisierungsbildschirm erscheint ohne Bibliotheksabhängigkeit,
2. lokale jQuery-Datei wird geladen und auf Version 4.0.0 geprüft,
3. lokales jQuery UI 1.14.2 samt Base-Theme wird geladen und über `jQuery.ui.version` geprüft,
4. die Clientmodule werden sequenziell geladen,
5. `/api/health` prüft Server und SQLite,
6. der Client lädt die Routenliste,
7. anschließend wird die Position ermittelt und die Liste auf nahe Routen eingeschränkt,
8. die responsive Routenansicht wird freigeschaltet.
Scheitert ein Schritt, bleibt der Initialisierungsbildschirm mit einer konkreten Fehlermeldung und einem Wiederholungsbutton sichtbar.
## Lokale UI-Abhängigkeiten
Im Vendor-Verzeichnis liegen die zur Laufzeit verwendeten Dateien vollständig lokal:
```text
public/vendor/jquery/jquery-4.0.0.min.js
public/vendor/jquery/LICENSE.txt
public/vendor/jquery-ui/jquery-ui-1.14.2.min.js
public/vendor/jquery-ui/jquery-ui-1.14.2.min.css
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.
## REST-API
| Methode | Pfad | Zweck |
|---|---|---|
| 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
```text
public/ mobiler Client
js/bootstrap-loader.js lädt und prüft lokale Bibliotheken
vendor/ lokale jQuery-/jQuery-UI-Dateien samt Themebildern
src/routes/ REST-Routing
src/services/ GPX-, Geodaten-, Speicher- und Fachlogik
database/schema.sql SQLite-Schema
data/ lokale SQLite-Datei
storage/active/routes/ aktive GPX-, Bild- und Audiodateien
storage/trash/routes/ zum Löschen markierte Strecken
examples/ Beispiel-GPX
test/ Basistests
```
## Technische Hinweise
- Medienpfade werden relativ zu `storage/` gespeichert. So bleibt das Projekt verschiebbar.
- Dateiverschiebung und Datenbankänderung sind durch eine kompensierende Rückverschiebung gekoppelt: Schlägt die SQL-Transaktion fehl, wird das Verzeichnis an seinen vorherigen Ort zurückbewegt.
- GPX-Erweiterungen ergänzen die Trackpunkte in SQLite. Das Original-GPX bleibt im Skelett unverändert; ein späterer Exportdienst sollte aus den Datenbankpunkten eine konsolidierte GPX-Datei generieren.
- Schreibzugriffe sind noch nicht authentifiziert. Vor einem öffentlichen Einsatz sind Rollen, Login, CSRF-Schutz, Rate-Limits, Dateisignaturprüfung und ein Moderationsworkflow zwingend zu ergänzen.
- Der Server bindet standardmäßig nur an `127.0.0.1`; Zugriffe von anderen Geräten sind damit bewusst ausgeschlossen.
- Die Instanzbezeichnung `Atlas` erscheint im Startprotokoll, im Initialisierungsbildschirm und in `/api/health`.
- Für die Geolokalisierung sollte der Client über `http://127.0.0.1:47145` geöffnet werden. Ein frei aufgelöster Hostname wie `http://Atlas:47145` gilt in Browsern ohne HTTPS in der Regel nicht als sicherer Kontext.
## Nächste Ausbaustufen
- Administrationsoberfläche zum Zeichnen/Importieren von Routen und Platzieren der POIs
- Benutzer-, Schul- und Projektzuordnung mit Rollenmodell
- Offline-Cache/PWA für Wanderungen ohne Mobilfunkempfang
- Kartenansicht, GPX-Visualisierung und Abweichungswarnung
- Bildunterschriften, Sortierung und gezieltes Entfernen einzelner Medien
- Hintergrundbereinigung des Papierkorbs nach einer konfigurierbaren Aufbewahrungsfrist
- Integritätsjournal für Dateiverschiebungen und Wiederherstellungen

17
SECURITY.md Normal file
View File

@ -0,0 +1,17 @@
# Sicherheitshinweise
## Legacy-Projekt
Das historische PHP-Projekt enthielt fest eingetragene Datenbank-Zugangsdaten. Diese Werte wurden nicht in das neue Projekt übernommen. Bestehende Zugangsdaten des Altprojekts sollten rotiert und anschließend aus der Repository-Historie entfernt beziehungsweise als kompromittiert behandelt werden.
## Stand dieses Skeletts
Die Schreibendpunkte besitzen absichtlich noch keine Authentifizierung und dürfen so nicht öffentlich erreichbar gemacht werden. Vor einem Produktivbetrieb sind mindestens erforderlich:
- Login und Rollen für Lehrkräfte, Redakteure und Administration,
- CSRF-Schutz beziehungsweise tokenbasierte API-Authentifizierung,
- serverseitige Prüfung tatsächlicher Datei-Inhalte statt nur MIME-Typ und Endung,
- Begrenzung von Request-Rate, Speicherverbrauch und Medienabmessungen,
- Protokollierung sämtlicher Änderungen und Wiederherstellungen,
- HTTPS, sichere Header und ein restriktiver Reverse-Proxy,
- Datenschutzkonzept für Standortdaten und von Schülerinnen und Schülern erstellte Medien.

File diff suppressed because one or more lines are too long

View File

@ -1,39 +0,0 @@
// From: https://developer.mozilla.org/de/docs/AJAX/Einf%C3%BChrung
var ajax = function(target, callback) {
var http_request = false;
if (window.XMLHttpRequest) { // Mozilla, Safari,...
http_request = new XMLHttpRequest();
if (http_request.overrideMimeType) {
http_request.overrideMimeType('text/xml');
}
} else if (window.ActiveXObject) { // IE
try {
http_request = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
try {
http_request = new ActiveXObject("Microsoft.XMLHTTP");
} catch (e) {}
}
}
if (!http_request) {
alert('Ende :( Kann keine XMLHTTP-Instanz erzeugen');
return false;
}
http_request.onreadystatechange = gotResponse;
http_request.open('GET', target, true);
http_request.send(null);
function gotResponse() {
if (http_request.readyState == 4) {
if (http_request.status == 200) {
if(callback != null) callback(http_request.responseText);
} else {
alert('Bei dem Request ist ein Problem aufgetreten.');
}
}
}
}

View File

@ -1,6 +0,0 @@
var scriptSettings = {
debug: true,
showDump: true,
showConsole: false,
}

View File

@ -1,55 +0,0 @@
/****************************************************************************************************************
* CLASS console: helper to make console-object available if not present *
* implemented as singleton-object *
* *
* methods: debug(args: variable number and type of arguments):void - dump parameters to console *
* *
* remarks: creates a css-styled div fixed on bottom of visible screen, if console-object is created *
****************************************************************************************************************/
if (!console)
{
console = {
create: function() {
$('body').append($('<div id="console"></div>'));
$('head').append($([
'<style type="text/css">',
' #console {',
' position: fixed;',
' bottom: 0;',
' left: 0;',
' right: 0;',
' z-index: 9998;',
' height: 12em;',
' line-height: 1.2em;',
' font-size: 0.8em;',
' background-color: rgb(220,220,220);',
' overflow-y: scroll;',
' }',
'</style>'
].join("\n")));
},
debug: function() {
var time = new Date();
$.each(arguments, function(key, data) {
$('#console').html(
$('#console').html()
+(
'('
+ ('0' + time.getHours()).substr(-2)
+ ':'
+ ('0' + time.getMinutes()).substr(-2)
+ ':'
+ ('0' + time.getSeconds()).substr(-2)
+ ') '
+ JSON.stringify(data)
+ ' ['
+ typeof(data)
+ ']'
).replace(/[\r\n]{1,1}/g, '<br />')
+'<br />'
).scrollTop($("#console").prop("scrollHeight"))
});
}
}
}

View File

@ -1,57 +0,0 @@
/****************************************************************************************************************
* CLASS DB: contains all data about routes and linked pois *
* *
* Events: body.databaseLoaded(database:DB) *
* *
* Returns: Array of routes in format *
* Object{ *
* id:Number, *
* name:String, *
* image:String, *
* languages:Array(String), *
* pois:Array(Object{ *
* lat:Number, *
* lon:Number, *
* desc:String, *
* range:Number, *
* audio:Number *
* } *
* } *
****************************************************************************************************************/
function DB() {
var that = this;
this._routes = []; // format: {id:0, name:'', image:'', languages:[], pois:[{lat:0, lon:0, desc:'', range:0, audio:0}]}
this._completeTracks = Array();
this._init = function() {
$('#database-loading').slideDown({duration: 500});
console.debug('loading database');
$.getJSON('database.php', {'get':'routes'}, function(data) {
console.debug('database: routes received');
data.forEach(function(track) {
//console.debug(id, track);
that._completeTracks[track.id] = false;
$.getJSON('database.php', {'get':'pois','id':track.id}, function(pois) {
console.debug(track.name, pois);
// console.debug('database: '+pois.length+' pois for '+track.name+' received');
track.pois = pois;
that._routes[track.id] = (track);
that._completeTracks[track.id] = true;
//console.debug('track done:', track);
if (that._completeTracks.reduce(function(prev, curr) { return (prev && curr); }, true)) { //tracks complete ?
$('#database-loading').slideUp({duration: 500});
console.debug('database loaded');
$('body').trigger('databaseLoaded', that);
}
});
});
});
}
this._init();
return this._routes;
}

View File

@ -1,27 +0,0 @@
// From: http://phpperformance.de/javascript-event-onload-und-die-bessere-alternative/
//create onDomReady Event
window.onDomReady = initReady;
// Initialize event depending on browser
function initReady(fn)
{
//W3C-compliant browser
if(document.addEventListener) {
document.addEventListener("DOMContentLoaded", fn, false);
}
//IE
else {
document.onreadystatechange = function(){readyState(fn)}
}
}
//IE execute function
function readyState(func)
{
// DOM is ready
if(document.readyState == "interactive" || document.readyState == "complete")
{
func();
}
}

View File

@ -1,176 +0,0 @@
/****************************************************************************************************************
* CLASS geoLocation: helper-class for fetching geolocation (among others latitude, longitude) *
* implemented as singleton-object *
* *
* methods: init(void):void --> initialize object and start fetching position *
* startWatcher(void):void --> starts intervalled position-watching (ran by init) *
* getPosFromServer(void):void --> fetch GPS-position from server (actually one fixed point) *
* distance(Object{lat:Number ,lon:Number} [,Object{lat:Number ,lon:Number}]):Number *
* -> distance between two positions (or given and current one, if second one missing) *
* *
* events: body.geoLocationError:Object{ *
* error:Object{ *
* number:Number, *
* message:String *
* }, *
* position:Object{ *
* latitude:Number, *
* longitude:Number, *
* accuracy:Number, *
* timestamp:Number *
* } *
* } *
* body.geoLocationGot:Object{ *
* position:Object{ *
* latitude:Number, *
* longitude:Number, *
* accuracy:Number, *
* timestamp:Number *
* } *
* } *
****************************************************************************************************************/
var geoLocation = { // implement as singleton
latitude: null,
longitude: null,
accuracy: null,
timestamp: null,
_pwatcher: null,
_ptimer: null,
_disableWatcherAt: 50,
_config: {
enableHighAccuracy: true,
maximumAge: 10000, // 10 sec max cache lifetime (if geoposition cached)
timeout: 60000 // 60 sec timeout
},
// event listeners
_geoReady: function(position) {
console.debug('geolocation api present - activate watcher');
// this.startWatcher();
// this._gotLocation(position);
},
_geoError: function(error) {
console.debug('gelocation api not present or error');
this._handle_error(error);
},
_gotLocation: function(position) {
this.latitude = position.coords.latitude;
this.longitude = position.coords.longitude;
this.accuracy = position.coords.accuracy;
this.timestamp = position.timestamp;
if (this.accuracy <= this._disableWatcherAt) // disable with timeout, if accurate location found
{
console.debug('disable watcher for 60 seconds', this._pwatcher);
if (this._pwatcher !== null)
{
console.debug('watcher cleared');
navigator.geolocation.clearWatch(this._pwatcher);
this._pwatcher = null;
}
that = this;
this._ptimer = setTimeout(function() {that.startWatcher.apply(that, arguments);}, 60000);
}
console.debug('got Position: ' + this.latitude + ', ' + this.longitude + '(' + this.accuracy + 'm < ' + this._disableWatcherAt + ': ' + (this.accuracy <= this._disableWatcherAt) + ')');
$('body').trigger('geoLocationGot', {'position':{latitude: this.latitude, longitude: this.longitude, accuracy: this.accuracy, timestamp: this.timestamp}});
},
_handle_error: function(error) {
console.debug(error);
switch(error.code)
{
case error.PERMISSION_DENIED:
if (_pwatcher != 'null') navigator.geolocation.clearWatch(_pwatcher);
_pwatcher = 'null';
break;
case error.POSITION_UNAVAILABLE:
break;
case error.TIMEOUT:
break;
default:
break;
}
$('body').trigger('geoLocationError',{'error':error,'position':{latitude: this.latitude, longitude: this.longitude, accuracy: this.accuracy, timestamp: this.timestamp}});
},
init: function() {
var that = this;
if (navigator.geolocation) {
console.debug('test geolocation api');
navigator.geolocation.getCurrentPosition(
function() {that._geoReady.apply(that, arguments);},
function() {that._geoError.apply(that, arguments);},
this._config
);
}
},
startWatcher: function() {
console.debug('waiting for location');
that = this;
if (this._pwatcher !== null) navigator.geolocation.clearWatch(this._pwatcher);
clearTimeout(this._ptimer);
this._pwatcher = navigator.geolocation.watchPosition(
function() {that._gotLocation.apply(that, arguments);},
function() {that._handle_error.apply(that, arguments);},
this._config
);
console.debug('watcher started',this._pwatcher);
},
getPosFromServer: function() {
console.debug('get position from server');
that = this;
$.getJSON('shippos.php', function(data) {
console.debug('got server response: ',data);
that._gotLocation.call(that, data);
});
},
//haversine formula for distance between two positions in m (see http://www.movable-type.co.uk/scripts/latlong.html)
distance:function(c1,c2)
{
PI180=Math.PI / 180;
if(!c1) return false;
if(!c2){ c2={'latitude':this.latitude, 'longitude':this.longitude}; }
var dLat = (c2.latitude-c1.latitude) * PI180; // delta in rad
var dLon = (c2.longitude-c1.longitude) * PI180;
var lat1 = c1.latitude * PI180;
var lat2 = c2.latitude * PI180;
var a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.sin(dLon/2) * Math.sin(dLon/2) * Math.cos(lat1) * Math.cos(lat2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return (6371000 * c); // earth-radius in m * 1000
}
}; // geoLocation
$(document).ready(function() {
$('body').on('geoLocationGot', function(e) {
$('#no-gps').slideUp({duration: 500});
});
$('body').on('geoLocationError',function(e) {
$('#no-gps').slideDown({duration: 500});
});
geoLocation.init();
});

View File

@ -1,39 +0,0 @@
/****************************************************************************************************************
* CLASS vibrate: helper-class for using vibration on mobiles *
* *
* methods: start(duration:Number or Array(Number)):void --> starts vibrating for given amount of ms *
* -> may also be an array with format: [vibrate,pause,vibrate,pause...] in ms *
* *
* stop(void):void --> stops vibrating *
* *
* objects: patterns --> predefined vibration-patterns *
****************************************************************************************************************/
var vibrate = {
start: function(duration) {
if (duration==undefined || duration==null) duration = this.patterns.long;
duration = [duration].reduce(function(a, b) { return [].concat(a).concat(b); });
if (navigator.vibrate) {
navigator.vibrate(duration);
}
console.debug('vibrate:', duration);
},
stop: function() {
if (navigator.vibrate) {
console.debug('stop vibrating');
navigator.vibrate([]);
}
},
patterns: {
micro: 50,
mini: 100,
short: 200,
long: 2000,
alarm: [100,50,100],
poi: [2000,200,2000]
}
}

24
audiojs/audio.min.js vendored
View File

@ -1,24 +0,0 @@
(function(h,o,g){var p=function(){for(var b=/audio(.min)?.js.*/,a=document.getElementsByTagName("script"),c=0,d=a.length;c<d;c++){var e=a[c].getAttribute("src");if(b.test(e))return e.replace(b,"")}}();g[h]={instanceCount:0,instances:{},flashSource:' <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" id="$1" width="1" height="1" name="$1" style="position: absolute; left: -1px;"> <param name="movie" value="$2?playerInstance='+h+'.instances[\'$1\']&datetime=$3"> <param name="allowscriptaccess" value="always"> <embed name="$1" src="$2?playerInstance='+
h+'.instances[\'$1\']&datetime=$3" width="1" height="1" allowscriptaccess="always"> </object>',settings:{autoplay:false,loop:false,preload:true,imageLocation:p+"player-graphics.gif",swfLocation:p+"audiojs.swf",useFlash:function(){var b=document.createElement("audio");return!(b.canPlayType&&b.canPlayType("audio/mpeg;").replace(/no/,""))}(),hasFlash:function(){if(navigator.plugins&&navigator.plugins.length&&navigator.plugins["Shockwave Flash"])return true;else if(navigator.mimeTypes&&navigator.mimeTypes.length){var b=
navigator.mimeTypes["application/x-shockwave-flash"];return b&&b.enabledPlugin}else try{new ActiveXObject("ShockwaveFlash.ShockwaveFlash");return true}catch(a){}return false}(),createPlayer:{markup:' <div class="play-pause"> <p class="play"></p> <p class="pause"></p> <p class="loading"></p> <p class="error"></p> </div> <div class="scrubber"> <div class="progress"></div> <div class="loaded"></div> </div> <div class="time"> <em class="played">00:00</em>/<strong class="duration">00:00</strong> </div> <div class="error-message"></div>',
playPauseClass:"play-pause",scrubberClass:"scrubber",progressClass:"progress",loaderClass:"loaded",timeClass:"time",durationClass:"duration",playedClass:"played",errorMessageClass:"error-message",playingClass:"playing",loadingClass:"loading",errorClass:"error"},css:' .audiojs audio { position: absolute; left: -1px; } .audiojs { width: 460px; height: 36px; background: #404040; overflow: hidden; font-family: monospace; font-size: 12px; background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #444), color-stop(0.5, #555), color-stop(0.51, #444), color-stop(1, #444)); background-image: -moz-linear-gradient(center top, #444 0%, #555 50%, #444 51%, #444 100%); -webkit-box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.3); -moz-box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.3); -o-box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.3); box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.3); } .audiojs .play-pause { width: 25px; height: 40px; padding: 4px 6px; margin: 0px; float: left; overflow: hidden; border-right: 1px solid #000; } .audiojs p { display: none; width: 25px; height: 40px; margin: 0px; cursor: pointer; } .audiojs .play { display: block; } .audiojs .scrubber { position: relative; float: left; width: 280px; background: #5a5a5a; height: 14px; margin: 10px; border-top: 1px solid #3f3f3f; border-left: 0px; border-bottom: 0px; overflow: hidden; } .audiojs .progress { position: absolute; top: 0px; left: 0px; height: 14px; width: 0px; background: #ccc; z-index: 1; background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #ccc), color-stop(0.5, #ddd), color-stop(0.51, #ccc), color-stop(1, #ccc)); background-image: -moz-linear-gradient(center top, #ccc 0%, #ddd 50%, #ccc 51%, #ccc 100%); } .audiojs .loaded { position: absolute; top: 0px; left: 0px; height: 14px; width: 0px; background: #000; background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #222), color-stop(0.5, #333), color-stop(0.51, #222), color-stop(1, #222)); background-image: -moz-linear-gradient(center top, #222 0%, #333 50%, #222 51%, #222 100%); } .audiojs .time { float: left; height: 36px; line-height: 36px; margin: 0px 0px 0px 6px; padding: 0px 6px 0px 12px; border-left: 1px solid #000; color: #ddd; text-shadow: 1px 1px 0px rgba(0, 0, 0, 0.5); } .audiojs .time em { padding: 0px 2px 0px 0px; color: #f9f9f9; font-style: normal; } .audiojs .time strong { padding: 0px 0px 0px 2px; font-weight: normal; } .audiojs .error-message { float: left; display: none; margin: 0px 10px; height: 36px; width: 400px; overflow: hidden; line-height: 36px; white-space: nowrap; color: #fff; text-overflow: ellipsis; -o-text-overflow: ellipsis; -icab-text-overflow: ellipsis; -khtml-text-overflow: ellipsis; -moz-text-overflow: ellipsis; -webkit-text-overflow: ellipsis; } .audiojs .error-message a { color: #eee; text-decoration: none; padding-bottom: 1px; border-bottom: 1px solid #999; white-space: wrap; } .audiojs .play { background: url("$1") -2px -1px no-repeat; } .audiojs .loading { background: url("$1") -2px -31px no-repeat; } .audiojs .error { background: url("$1") -2px -61px no-repeat; } .audiojs .pause { background: url("$1") -2px -91px no-repeat; } .playing .play, .playing .loading, .playing .error { display: none; } .playing .pause { display: block; } .loading .play, .loading .pause, .loading .error { display: none; } .loading .loading { display: block; } .error .time, .error .play, .error .pause, .error .scrubber, .error .loading { display: none; } .error .error { display: block; } .error .play-pause p { cursor: auto; } .error .error-message { display: block; }',
trackEnded:function(){},flashError:function(){var b=this.settings.createPlayer,a=j(b.errorMessageClass,this.wrapper),c='Missing <a href="http://get.adobe.com/flashplayer/">flash player</a> plugin.';if(this.mp3)c+=' <a href="'+this.mp3+'">Download audio file</a>.';g[h].helpers.removeClass(this.wrapper,b.loadingClass);g[h].helpers.addClass(this.wrapper,b.errorClass);a.innerHTML=c},loadError:function(){var b=this.settings.createPlayer,a=j(b.errorMessageClass,this.wrapper);g[h].helpers.removeClass(this.wrapper,
b.loadingClass);g[h].helpers.addClass(this.wrapper,b.errorClass);a.innerHTML='Error loading: "'+this.mp3+'"'},init:function(){g[h].helpers.addClass(this.wrapper,this.settings.createPlayer.loadingClass)},loadStarted:function(){var b=this.settings.createPlayer,a=j(b.durationClass,this.wrapper),c=Math.floor(this.duration/60),d=Math.floor(this.duration%60);g[h].helpers.removeClass(this.wrapper,b.loadingClass);a.innerHTML=(c<10?"0":"")+c+":"+(d<10?"0":"")+d},loadProgress:function(b){var a=this.settings.createPlayer,
c=j(a.scrubberClass,this.wrapper);j(a.loaderClass,this.wrapper).style.width=c.offsetWidth*b+"px"},playPause:function(){this.playing?this.settings.play():this.settings.pause()},play:function(){g[h].helpers.addClass(this.wrapper,this.settings.createPlayer.playingClass)},pause:function(){g[h].helpers.removeClass(this.wrapper,this.settings.createPlayer.playingClass)},updatePlayhead:function(b){var a=this.settings.createPlayer,c=j(a.scrubberClass,this.wrapper);j(a.progressClass,this.wrapper).style.width=
c.offsetWidth*b+"px";a=j(a.playedClass,this.wrapper);c=this.duration*b;b=Math.floor(c/60);c=Math.floor(c%60);a.innerHTML=(b<10?"0":"")+b+":"+(c<10?"0":"")+c}},create:function(b,a){a=a||{};return b.length?this.createAll(a,b):this.newInstance(b,a)},createAll:function(b,a){var c=a||document.getElementsByTagName("audio"),d=[];b=b||{};for(var e=0,i=c.length;e<i;e++)d.push(this.newInstance(c[e],b));return d},newInstance:function(b,a){var c=this.helpers.clone(this.settings),d="audiojs"+this.instanceCount,
e="audiojs_wrapper"+this.instanceCount;this.instanceCount++;if(b.getAttribute("autoplay")!=null)c.autoplay=true;if(b.getAttribute("loop")!=null)c.loop=true;if(b.getAttribute("preload")=="none")c.preload=false;a&&this.helpers.merge(c,a);if(c.createPlayer.markup)b=this.createPlayer(b,c.createPlayer,e);else b.parentNode.setAttribute("id",e);e=new g[o](b,c);c.css&&this.helpers.injectCss(e,c.css);if(c.useFlash&&c.hasFlash){this.injectFlash(e,d);this.attachFlashEvents(e.wrapper,e)}else c.useFlash&&!c.hasFlash&&
this.settings.flashError.apply(e);if(!c.useFlash||c.useFlash&&c.hasFlash)this.attachEvents(e.wrapper,e);return this.instances[d]=e},createPlayer:function(b,a,c){var d=document.createElement("div"),e=b.cloneNode(true);d.setAttribute("class","audiojs");d.setAttribute("className","audiojs");d.setAttribute("id",c);if(e.outerHTML&&!document.createElement("audio").canPlayType){e=this.helpers.cloneHtml5Node(b);d.innerHTML=a.markup;d.appendChild(e);b.outerHTML=d.outerHTML;d=document.getElementById(c)}else{d.appendChild(e);
d.innerHTML+=a.markup;b.parentNode.replaceChild(d,b)}return d.getElementsByTagName("audio")[0]},attachEvents:function(b,a){if(a.settings.createPlayer){var c=a.settings.createPlayer,d=j(c.playPauseClass,b),e=j(c.scrubberClass,b);g[h].events.addListener(d,"click",function(){a.playPause.apply(a)});g[h].events.addListener(e,"click",function(i){i=i.clientX;var f=this,k=0;if(f.offsetParent){do k+=f.offsetLeft;while(f=f.offsetParent)}a.skipTo((i-k)/e.offsetWidth)});if(!a.settings.useFlash){g[h].events.trackLoadProgress(a);
g[h].events.addListener(a.element,"timeupdate",function(){a.updatePlayhead.apply(a)});g[h].events.addListener(a.element,"ended",function(){a.trackEnded.apply(a)});g[h].events.addListener(a.source,"error",function(){clearInterval(a.readyTimer);clearInterval(a.loadTimer);a.settings.loadError.apply(a)})}}},attachFlashEvents:function(b,a){a.swfReady=false;a.load=function(c){a.mp3=c;a.swfReady&&a.element.load(c)};a.loadProgress=function(c,d){a.loadedPercent=c;a.duration=d;a.settings.loadStarted.apply(a);
a.settings.loadProgress.apply(a,[c])};a.skipTo=function(c){if(!(c>a.loadedPercent)){a.updatePlayhead.call(a,[c]);a.element.skipTo(c)}};a.updatePlayhead=function(c){a.settings.updatePlayhead.apply(a,[c])};a.play=function(){if(!a.settings.preload){a.settings.preload=true;a.element.init(a.mp3)}a.playing=true;a.element.pplay();a.settings.play.apply(a)};a.pause=function(){a.playing=false;a.element.ppause();a.settings.pause.apply(a)};a.setVolume=function(c){a.element.setVolume(c)};a.loadStarted=function(){a.swfReady=
true;a.settings.preload&&a.element.init(a.mp3);a.settings.autoplay&&a.play.apply(a)}},injectFlash:function(b,a){var c=this.flashSource.replace(/\$1/g,a);c=c.replace(/\$2/g,b.settings.swfLocation);c=c.replace(/\$3/g,+new Date+Math.random());var d=b.wrapper.innerHTML,e=document.createElement("div");e.innerHTML=c+d;b.wrapper.innerHTML=e.innerHTML;b.element=this.helpers.getSwf(a)},helpers:{merge:function(b,a){for(attr in a)if(b.hasOwnProperty(attr)||a.hasOwnProperty(attr))b[attr]=a[attr]},clone:function(b){if(b==
null||typeof b!=="object")return b;var a=new b.constructor,c;for(c in b)a[c]=arguments.callee(b[c]);return a},addClass:function(b,a){RegExp("(\\s|^)"+a+"(\\s|$)").test(b.className)||(b.className+=" "+a)},removeClass:function(b,a){b.className=b.className.replace(RegExp("(\\s|^)"+a+"(\\s|$)")," ")},injectCss:function(b,a){for(var c="",d=document.getElementsByTagName("style"),e=a.replace(/\$1/g,b.settings.imageLocation),i=0,f=d.length;i<f;i++){var k=d[i].getAttribute("title");if(k&&~k.indexOf("audiojs")){f=
d[i];if(f.innerHTML===e)return;c=f.innerHTML;break}}d=document.getElementsByTagName("head")[0];i=d.firstChild;f=document.createElement("style");if(d){f.setAttribute("type","text/css");f.setAttribute("title","audiojs");if(f.styleSheet)f.styleSheet.cssText=c+e;else f.appendChild(document.createTextNode(c+e));i?d.insertBefore(f,i):d.appendChild(styleElement)}},cloneHtml5Node:function(b){var a=document.createDocumentFragment(),c=a.createElement?a:document;c.createElement("audio");c=c.createElement("div");
a.appendChild(c);c.innerHTML=b.outerHTML;return c.firstChild},getSwf:function(b){b=document[b]||window[b];return b.length>1?b[b.length-1]:b}},events:{memoryLeaking:false,listeners:[],addListener:function(b,a,c){if(b.addEventListener)b.addEventListener(a,c,false);else if(b.attachEvent){this.listeners.push(b);if(!this.memoryLeaking){window.attachEvent("onunload",function(){if(this.listeners)for(var d=0,e=this.listeners.length;d<e;d++)g[h].events.purge(this.listeners[d])});this.memoryLeaking=true}b.attachEvent("on"+
a,function(){c.call(b,window.event)})}},trackLoadProgress:function(b){if(b.settings.preload){var a,c;b=b;var d=/(ipod|iphone|ipad)/i.test(navigator.userAgent);a=setInterval(function(){if(b.element.readyState>-1)d||b.init.apply(b);if(b.element.readyState>1){b.settings.autoplay&&b.play.apply(b);clearInterval(a);c=setInterval(function(){b.loadProgress.apply(b);b.loadedPercent>=1&&clearInterval(c)})}},10);b.readyTimer=a;b.loadTimer=c}},purge:function(b){var a=b.attributes,c;if(a)for(c=0;c<a.length;c+=
1)if(typeof b[a[c].name]==="function")b[a[c].name]=null;if(a=b.childNodes)for(c=0;c<a.length;c+=1)purge(b.childNodes[c])},ready:function(){return function(b){var a=window,c=false,d=true,e=a.document,i=e.documentElement,f=e.addEventListener?"addEventListener":"attachEvent",k=e.addEventListener?"removeEventListener":"detachEvent",n=e.addEventListener?"":"on",m=function(l){if(!(l.type=="readystatechange"&&e.readyState!="complete")){(l.type=="load"?a:e)[k](n+l.type,m,false);if(!c&&(c=true))b.call(a,l.type||
l)}},q=function(){try{i.doScroll("left")}catch(l){setTimeout(q,50);return}m("poll")};if(e.readyState=="complete")b.call(a,"lazy");else{if(e.createEventObject&&i.doScroll){try{d=!a.frameElement}catch(r){}d&&q()}e[f](n+"DOMContentLoaded",m,false);e[f](n+"readystatechange",m,false);a[f](n+"load",m,false)}}}()}};g[o]=function(b,a){this.element=b;this.wrapper=b.parentNode;this.source=b.getElementsByTagName("source")[0]||b;this.mp3=function(c){var d=c.getElementsByTagName("source")[0];return c.getAttribute("src")||
(d?d.getAttribute("src"):null)}(b);this.settings=a;this.loadStartedCalled=false;this.loadedPercent=0;this.duration=1;this.playing=false};g[o].prototype={updatePlayhead:function(){this.settings.updatePlayhead.apply(this,[this.element.currentTime/this.duration])},skipTo:function(b){if(!(b>this.loadedPercent)){this.element.currentTime=this.duration*b;this.updatePlayhead()}},load:function(b){this.loadStartedCalled=false;this.source.setAttribute("src",b);this.element.load();this.mp3=b;g[h].events.trackLoadProgress(this)},
loadError:function(){this.settings.loadError.apply(this)},init:function(){this.settings.init.apply(this)},loadStarted:function(){if(!this.element.duration)return false;this.duration=this.element.duration;this.updatePlayhead();this.settings.loadStarted.apply(this)},loadProgress:function(){if(this.element.buffered!=null&&this.element.buffered.length){if(!this.loadStartedCalled)this.loadStartedCalled=this.loadStarted();this.loadedPercent=this.element.buffered.end(this.element.buffered.length-1)/this.duration;
this.settings.loadProgress.apply(this,[this.loadedPercent])}},playPause:function(){this.playing?this.pause():this.play()},play:function(){/(ipod|iphone|ipad)/i.test(navigator.userAgent)&&this.element.readyState==0&&this.init.apply(this);if(!this.settings.preload){this.settings.preload=true;this.element.setAttribute("preload","auto");g[h].events.trackLoadProgress(this)}this.playing=true;this.element.play();this.settings.play.apply(this)},pause:function(){this.playing=false;this.element.pause();this.settings.pause.apply(this)},
setVolume:function(b){this.element.volume=b},trackEnded:function(){this.skipTo.apply(this,[0]);this.settings.loop||this.pause.apply(this);this.settings.trackEnded.apply(this)}};var j=function(b,a){var c=[];a=a||document;if(a.getElementsByClassName)c=a.getElementsByClassName(b);else{var d,e,i=a.getElementsByTagName("*"),f=RegExp("(^|\\s)"+b+"(\\s|$)");d=0;for(e=i.length;d<e;d++)f.test(i[d].className)&&c.push(i[d])}return c.length>1?c:c[0]}})("audiojs","audiojsInstance",this);

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

0
data/.gitkeep Normal file
View File

View File

@ -1,43 +0,0 @@
<?php
require_once('db.inc.php');
$ret_data = Array();
$get = trim(strtolower($_GET['get']));
$id = intval($_GET['id']);
switch($get)
{
case 'routes':
$data = db_select('routes',Array('idx','name','image','languages'),['idx'=>'<=2'],Array('name'));
//echo utf8_decode(print_r($data, true));
//$ret_data = $data;
foreach($data as $row)
{
// format: {id:0, name:'', image:'', languages:[], pois:[{lat:0, lon:0, desc:'', range:0, audio:0}]}}}
$line = Array('id'=>($row['idx']), 'name'=>('Fahrtrichtung '.preg_replace('/.*\(([a-zäöüß]+)\)/i', '$1', $row['name'])), 'image'=>($row['image']), 'languages'=>preg_split('/[, |]+/',($row['languages'])), pois=>Array());
// print(print_r($line, true)."<br/>\n");
$ret_data[] = $line;
}
break;
case 'pois':
$data = db_select('trkpts',Array('poi_id','file_id','GREATEST(`trkpts.range`,`pois.range`) AS range', 'desc', 'lat','lon', 'display_text_de'), Array('route_id'=>$id), null, Array('pois'=>Array('poi_id','idx')));
foreach($data as $row)
{
// format: [{lat:0, lon:0, desc:'', range:0, audio:0}]
$ret_data[$row['poi_id']] = Array(
'lat'=>utf8_decode($row['lat']),
'lon'=>utf8_decode($row['lon']),
'desc'=>utf8_decode($row['desc']),
'range'=>utf8_decode($row['range']),
'text'=>$row['display_text_de'],
'audio'=>utf8_decode($row['file_id'])
);
}
break;
}
echo(json_encode($ret_data));
//exit("<br />\n".json_last_error());

63
database/schema.sql Normal file
View File

@ -0,0 +1,63 @@
CREATE TABLE IF NOT EXISTS routes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
school_name TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'deleted')),
gpx_path TEXT NOT NULL,
start_lat REAL,
start_lon REAL,
center_lat REAL,
center_lon REAL,
min_lat REAL,
min_lon REAL,
max_lat REAL,
max_lon REAL,
distance_m REAL NOT NULL DEFAULT 0,
elevation_gain_m REAL NOT NULL DEFAULT 0,
point_count INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TEXT,
trash_path TEXT
) STRICT;
CREATE TABLE IF NOT EXISTS route_points (
id INTEGER PRIMARY KEY AUTOINCREMENT,
route_id INTEGER NOT NULL REFERENCES routes(id),
sequence INTEGER NOT NULL,
lat REAL NOT NULL,
lon REAL NOT NULL,
elevation REAL,
recorded_at TEXT,
UNIQUE(route_id, sequence)
) STRICT;
CREATE TABLE IF NOT EXISTS pois (
id INTEGER PRIMARY KEY AUTOINCREMENT,
route_id INTEGER NOT NULL REFERENCES routes(id),
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
lat REAL NOT NULL,
lon REAL NOT NULL,
trigger_radius_m REAL NOT NULL DEFAULT 80,
sequence INTEGER NOT NULL DEFAULT 0,
audio_path TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
) STRICT;
CREATE TABLE IF NOT EXISTS poi_images (
id INTEGER PRIMARY KEY AUTOINCREMENT,
poi_id INTEGER NOT NULL REFERENCES pois(id),
path TEXT NOT NULL,
caption TEXT NOT NULL DEFAULT '',
sequence INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
) STRICT;
CREATE INDEX IF NOT EXISTS idx_routes_status ON routes(status);
CREATE INDEX IF NOT EXISTS idx_route_points_route ON route_points(route_id, sequence);
CREATE INDEX IF NOT EXISTS idx_pois_route ON pois(route_id, sequence);
CREATE INDEX IF NOT EXISTS idx_poi_images_poi ON poi_images(poi_id, sequence);

View File

@ -1,114 +0,0 @@
<?php
define('DB_HOST','mysql1.mediaproject.de');
define('DB_USER','c30_sds_api');
define('DB_PASS','sBF77apxdcpfwJcAk28o');
define('DB_DATABASE','c30_sds_api');
$DB=new PDO('mysql:host='.DB_HOST.';dbname='.DB_DATABASE.';chatset=utf-8',DB_USER,DB_PASS);
$DB->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
$DB->query('SET NAMES \'utf8\'');
function db_select($table, $data, $where=[], $order=[], $join=[], $debug=false)
{
global $DB;
$STH = "";
if(gettype($data)=='string') $data = array($data);
$data = '`'.implode('`,`', $data).'`';
$data = str_replace('`*`','*', $data);
if(!empty($where) && is_array($where))
{
$cond = [];
foreach($where as $key=>$value) { $cond[] = "`$key`='$value'"; }
$where = implode(' AND ', $cond);
$where = str_replace('=\'!', '!=\'',$where);
$where = str_replace('AND `|', 'OR `', $where);
$where = preg_replace('#=\s*[\'\"]([<>=]+)(.*?)[\'\"]#', '$1 \'$2\'', $where);
}
if(!empty($order) && is_array($order))
{
// format: ['field1'=>'direction','field2'=>'direction'...] OR ['field1', 'field2'...]
$cond = [];
foreach($order as $field=>$direction) {
if(is_numeric($field)) {
$field = $direction;
$direction='ASC';
}
$cond[] = $field.' '.$direction;
}
$order = implode(',',$cond);
}
if(!empty($join) && is_array($join))
{
// format: ['table1'=>['field-of-source','field-of-table1'], 'table2'=>['field-of-source','field-of-table2']]
$cond = [];
foreach($join as $key=>$value)
{
if(is_array($value) && count($value)==2)
{
$cond[] = str_replace('~', $key, ' JOIN '.$key.' ON '.$table.'.'.implode(' = ~.',$value));
}
}
$join = implode('',$cond);
}
$data = preg_replace('#`([^`]+?)\.([^`]+?)`#', '$1.`$2`', $data);
$data = preg_replace('#`(.+?) as (.*?)`#i','`$1` AS `$2`', $data);
$data = preg_replace('#`([a-z0-9]+\(.*?\))`#i', '$1', $data);
$where = preg_replace('#`([^`]+?)\.([^`]+?)`#', '$1.`$2`', $where);
$order = preg_replace('#`([^`]+?)\.([^`]+?)`#', '$1.`$2`', $order);
$join = preg_replace('#`([^`]+?)\.([^`]+?)`#', '$1.`$2`', $join);
if(empty($where)) $where = '';
if(empty($order)) $order = '';
if(empty($join)) $join = '';
$sql = 'SELECT '.$data.' FROM '.$table.$join.(!empty($where) ? ' WHERE '.$where : '').(!empty($order) ? ' ORDER BY '.$order : '');
//exit($sql);
try{
$STH = $DB->prepare($sql);
$STH->execute();
$retVal = $STH->fetchAll(PDO::FETCH_ASSOC);
if($debug===true)
{
$dump = print_r($STH, true).": ".print_r($retVal, true);
print($dump.'<br>');
file_put_contents("query_debug.txt", $dump."\r\n", FILE_APPEND);
}
return $retVal;
}
catch(PDOException $err)
{
print_r($STH); print('<br/>');
print_r($err);
die();
}
}
function getOptions($data, $debug=false)
{
global $DB;
$STH = "";
$sql = 'SELECT * FROM options WHERE name IN (\''.implode('\',\'',$data).'\');';
try {
$STH = $DB->prepare($sql);
$STH->execute($data);
if($debug===true)
{
$dump = print_r($STH);
print($dump); print('<br>');
file_put_contents("query_debug.txt", $dump."\r\n", FILE_APPEND);
}
return $STH->fetchAll(PDO::FETCH_ASSOC);
} catch(Exception $err) {
print_r($STH); print('<br/>');
print_r($err);
}
}

25
docs/legacy-migration.md Normal file
View File

@ -0,0 +1,25 @@
# Migration vom historischen Wegwichtel-Projekt
## Übernommene Fachkonzepte
- ereignisbasierte Verarbeitung neuer GPS-Positionen,
- Distanzberechnung zwischen Standort und POIs,
- Aktivierung einer Station beim Erreichen eines Radius,
- Auswahl verschiedener Strecken,
- Audioansagen und eine mobile Darstellung.
## Ersetzte technische Bestandteile
| Altprojekt | Neues Skelett |
|---|---|
| PHP-Endpunkte und MySQL | Express-REST-API und lokale SQLite-Datei |
| AudioJS/Flash-Fallback | natives HTML5-`audio`-Element |
| globale Zustände auf `document` | gekapselter Clientzustand in `Wegwichtel.App` |
| hart codierte Audiodateinamen | in SQLite gespeicherte relative Medienpfade |
| fester Auslöseradius | Radius pro POI |
| unmittelbar gelöschte/extern verwaltete Dateien | Soft Delete mit Papierkorb und Wiederherstellung |
| statisch eingebundene Skripte | Initialisierungsloader mit Selbsttests und Fehleranzeige |
## Bewusste Übergangstechnologie
jQuery Mobile wurde entfernt. Die responsive Oberfläche verwendet jQuery 4.0.0 und jQuery UI 1.14.2; die Seitenumschaltung erfolgt über eine kleine History-API-basierte Navigation. GPS-, API-, Audio- und Diashowlogik bleiben von der UI-Schicht getrennt.

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="Wegwichtel" xmlns="http://www.topografix.com/GPX/1/1">
<metadata><name>Beispielweg</name></metadata>
<trk><name>Beispielweg</name><trkseg>
<trkpt lat="52.5200" lon="13.4050"><ele>34</ele></trkpt>
<trkpt lat="52.5208" lon="13.4070"><ele>37</ele></trkpt>
<trkpt lat="52.5216" lon="13.4090"><ele>35</ele></trkpt>
</trkseg></trk>
</gpx>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 B

View File

@ -1,47 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="initial-scale=1.0, width=320, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<title>SDS Mobile</title>
<script type="text/javascript" src="jquery/jquery-1.10.2.js"></script>
<script type="text/javascript" src="audiojs/audio.min.js"></script>
<script type="text/javascript" src="api/config.js"></script>
<script type="text/javascript" src="api/vibrate.js"></script>
<script type="text/javascript" src="api/geolocation.js"></script>
<script type="text/javascript" src="api/database.js"></script>
<link rel="stylesheet" type="text/css" href="layout.css" />
</head>
<body>
<h1 class="header">Mobiles Ansagesystem</h1>
<div id="hints">
<div id="no-gps">Es konnte noch keine GPS-Position ermittelt werden.</div>
<div id="database-loading">Bitte warten Sie, bis die Datenbank geladen wurde.</div>
</div>
<div id="dump"></div>
<div id="routeSelection">
<label for="routes">Strecken:</label>
<select id="routes" onchange="trackChanged(this.value);">
<option value="" selected>keine Strecke verfügbar</option>
</select>
</div>
<div id="poiViews">
</div>
<ul id="trackDump">
</ul>
<div id="audio"></div>
<ul id="contextMenu">
<li><a href="#" id="contextReload">Seite neu laden</a></li>
<li><a href="#" id="contextPlay">Ansage abspielen</a></li>
</ul>
<script type="text/javascript" src="api/console.js"></script>
<script type="text/javascript" src="script.js"></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 B

Some files were not shown because too many files have changed in this diff Show More