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 |