initial commit
8
.env.example
Normal 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
@ -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
@ -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
@ -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.
|
||||
810
api/adminer.php
39
api/ajax.js
@ -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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
var scriptSettings = {
|
||||
debug: true,
|
||||
|
||||
showDump: true,
|
||||
showConsole: false,
|
||||
}
|
||||
@ -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"))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
});
|
||||
@ -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
@ -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);
|
||||
|
Before Width: | Height: | Size: 4.4 KiB |
0
data/.gitkeep
Normal file
43
database.php
@ -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
@ -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);
|
||||
114
db.inc.php
@ -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
@ -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.
|
||||
9
examples/sample-route.gpx
Normal 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>
|
||||
BIN
img/checked.png
|
Before Width: | Height: | Size: 672 B |
BIN
img/play.png
|
Before Width: | Height: | Size: 286 B |
BIN
img/speaker.png
|
Before Width: | Height: | Size: 1.5 KiB |
BIN
img/stop.png
|
Before Width: | Height: | Size: 150 B |
47
index.html
@ -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>
|
||||
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 219 B |
|
Before Width: | Height: | Size: 227 B |
|
Before Width: | Height: | Size: 244 B |
|
Before Width: | Height: | Size: 243 B |
|
Before Width: | Height: | Size: 146 B |
|
Before Width: | Height: | Size: 167 B |
|
Before Width: | Height: | Size: 173 B |
|
Before Width: | Height: | Size: 159 B |
|
Before Width: | Height: | Size: 171 B |
|
Before Width: | Height: | Size: 149 B |
|
Before Width: | Height: | Size: 149 B |
|
Before Width: | Height: | Size: 156 B |
|
Before Width: | Height: | Size: 147 B |
|
Before Width: | Height: | Size: 152 B |
|
Before Width: | Height: | Size: 147 B |
|
Before Width: | Height: | Size: 163 B |
|
Before Width: | Height: | Size: 169 B |
|
Before Width: | Height: | Size: 163 B |
|
Before Width: | Height: | Size: 165 B |
|
Before Width: | Height: | Size: 151 B |
|
Before Width: | Height: | Size: 307 B |
|
Before Width: | Height: | Size: 314 B |
|
Before Width: | Height: | Size: 233 B |
|
Before Width: | Height: | Size: 240 B |
|
Before Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 135 B |
|
Before Width: | Height: | Size: 147 B |
|
Before Width: | Height: | Size: 152 B |
|
Before Width: | Height: | Size: 146 B |
|
Before Width: | Height: | Size: 143 B |
|
Before Width: | Height: | Size: 250 B |
|
Before Width: | Height: | Size: 251 B |
|
Before Width: | Height: | Size: 207 B |
|
Before Width: | Height: | Size: 213 B |
|
Before Width: | Height: | Size: 174 B |
|
Before Width: | Height: | Size: 177 B |
|
Before Width: | Height: | Size: 184 B |
|
Before Width: | Height: | Size: 194 B |
|
Before Width: | Height: | Size: 196 B |
|
Before Width: | Height: | Size: 204 B |
|
Before Width: | Height: | Size: 169 B |
|
Before Width: | Height: | Size: 172 B |
|
Before Width: | Height: | Size: 310 B |
|
Before Width: | Height: | Size: 316 B |
|
Before Width: | Height: | Size: 212 B |
|
Before Width: | Height: | Size: 210 B |
|
Before Width: | Height: | Size: 165 B |
|
Before Width: | Height: | Size: 160 B |
|
Before Width: | Height: | Size: 171 B |
|
Before Width: | Height: | Size: 185 B |
|
Before Width: | Height: | Size: 163 B |
|
Before Width: | Height: | Size: 170 B |
|
Before Width: | Height: | Size: 249 B |
|
Before Width: | Height: | Size: 253 B |
|
Before Width: | Height: | Size: 299 B |
|
Before Width: | Height: | Size: 308 B |
|
Before Width: | Height: | Size: 233 B |
|
Before Width: | Height: | Size: 243 B |
|
Before Width: | Height: | Size: 318 B |
|
Before Width: | Height: | Size: 302 B |
|
Before Width: | Height: | Size: 160 B |
|
Before Width: | Height: | Size: 167 B |
|
Before Width: | Height: | Size: 242 B |
|
Before Width: | Height: | Size: 246 B |
|
Before Width: | Height: | Size: 150 B |
|
Before Width: | Height: | Size: 154 B |
|
Before Width: | Height: | Size: 250 B |
|
Before Width: | Height: | Size: 251 B |
|
Before Width: | Height: | Size: 245 B |
|
Before Width: | Height: | Size: 247 B |
|
Before Width: | Height: | Size: 204 B |
|
Before Width: | Height: | Size: 207 B |
|
Before Width: | Height: | Size: 226 B |