1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 // Get the best-available objects for indexedDB and requestAnimationFrame.
6 window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB;
7 window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
10 var gAppInitDone = false;
12 var gUIHideCountdown = 0;
14 var gTrackUpdateInterval;
15 var gAction, gActionLabel;
16 var authData = null, userData = null;
17 var gBackendURL = "https://backend.lantea.kairo.at/";
18 var gAuthClientID = "lantea";
20 window.onload = function() {
21 gAction = document.getElementById("action");
22 gActionLabel = document.getElementById("actionlabel");
24 var mSel = document.getElementById("mapSelector");
25 for (var mapStyle in gMapStyles) {
26 var opt = document.createElement("option");
28 opt.text = gMapStyles[mapStyle].name;
32 var areas = document.getElementsByClassName("autoFade");
33 for (var i = 0; i <= areas.length - 1; i++) {
34 areas[i].addEventListener("mouseup", uiEvHandler, false);
35 areas[i].addEventListener("mousemove", uiEvHandler, false);
36 areas[i].addEventListener("mousedown", uiEvHandler, false);
37 areas[i].addEventListener("mouseout", uiEvHandler, false);
39 areas[i].addEventListener("touchstart", uiEvHandler, false);
40 areas[i].addEventListener("touchmove", uiEvHandler, false);
41 areas[i].addEventListener("touchend", uiEvHandler, false);
42 areas[i].addEventListener("touchcancel", uiEvHandler, false);
43 areas[i].addEventListener("touchleave", uiEvHandler, false);
46 document.getElementById("body").addEventListener("keydown", uiEvHandler, false);
48 if (navigator.platform.length == "") {
49 // For Firefox OS, don't display the "save" button.
50 // Do this by setting the debugHide class for testing in debug mode.
51 document.getElementById("saveTrackButton").classList.add("debugHide");
54 // Set backend URL in a way that it works for testing on localhost as well as
55 // both the lantea.kairo.at and lantea-dev.kairo.at deployments.
56 if (window.location.host == "localhost") {
57 gBackendURL = window.location.protocol + '//' + window.location.host + "/lantea-backend/";
60 gBackendURL = window.location.protocol + '//' + "backend." + window.location.host + "/";
62 // Make sure to use a different login client ID for the -dev setup.
63 if (/\-dev\./.test(window.location.host)) {
64 gAuthClientID += "-dev";
67 document.getElementById("libCloseButton").onclick = hideLibrary;
69 // Set up the login area.
70 document.getElementById("loginbtn").onclick = startLogin;
71 document.getElementById("logoutbtn").onclick = doLogout;
72 // Put in a logged-out state by default.
73 // Opening the track drawer will update this correctly.
76 gAction.addEventListener("dbinit-done", initMap, false);
77 gAction.addEventListener("mapinit-done", postInit, false);
78 console.log("starting DB init...");
82 function postInit(aEvent) {
83 gAction.removeEventListener(aEvent.type, postInit, false);
84 console.log("init done, draw map.");
85 gMapPrefsLoaded = true;
87 //gMap.resizeAndDraw(); <-- HACK: This triggers bug 1001853, work around with a delay.
88 window.setTimeout(gMap.resizeAndDraw, 100);
89 gActionLabel.textContent = "";
90 gAction.style.display = "none";
91 setTracking(document.getElementById("trackCheckbox"));
92 gPrefs.get("devicename", function(aValue) {
94 document.getElementById("uploadDevName").value = aValue;
101 gPrefs.get("lastInfoShown", function(aValue) {
102 if (!aValue || !parseInt(aValue) || parseInt(aValue) < 1) {
107 gPrefs.set("lastInfoShown", 1);
110 window.onresize = function() {
111 gMap.resizeAndDraw();
114 function startLogin() {
115 var logerr = document.getElementById("loginerror");
116 logerr.classList.add("hidden");
118 if (!authData || !authData["state"]) {
119 // We have no oAuth state, try to fetch it and call ourselves again if it worked.
120 prepareLoginButton(function() {
121 if (authData && authData["state"]) {
124 else if (!userData) {
125 // Only warn if we didn't actually end up being logged in.
126 console.log("No OAuth state and fetching fails, client or server may be offline.");
127 logerr.classList.remove("hidden");
128 logerr.title = "Client or server may be offline.";
133 var authURL = authData["url"] + "authorize?response_type=code&client_id=" + gAuthClientID + "&scope=email" +
134 "&state=" + authData["state"] + "&redirect_uri=" + encodeURIComponent(getRedirectURI());
135 if (window.open(authURL, "KaiRoAuth", 'height=450,width=600')) {
136 console.log("Sign In window open.");
139 console.log("Opening Sign In window failed.");
140 logerr.classList.remove("hidden");
141 logerr.title = "Opening Sign-In window failed.";
145 function getRedirectURI() {
146 return window.location.protocol + '//' + window.location.host + window.location.pathname.replace("index.html", "") + "login.html";
149 function doLogout() {
150 fetchBackend("logout", "GET", null,
151 function(aResult, aStatus) {
153 prepareLoginButton();
156 console.log("Backend issue trying to log out.");
163 function prepareLoginButton(aCallback) {
164 fetchBackend("oauth_state", "GET", null,
165 function(aResult, aStatus) {
166 if (aStatus == 200) {
167 if (aResult["logged_in"]) {
169 "email": aResult["email"],
170 "permissions": aResult["permissions"],
176 authData = {"state": aResult["state"], "url": aResult["url"]};
182 console.log("Backend error " + aStatus + " fetching OAuth state: " + aResult["message"]);
184 if (aCallback) { aCallback(); }
190 function finishLogin(aCode, aState) {
191 if (aState == authData["state"]) {
192 fetchBackend("login?code=" + aCode + "&state=" + aState + "&redirect_uri=" + encodeURIComponent(getRedirectURI()), "GET", null,
193 function(aResult, aStatus) {
194 if (aStatus == 200) {
196 "email": aResult["email"],
197 "permissions": aResult["permissions"],
202 console.log("Login error " + aStatus + ": " + aResult["message"]);
203 prepareLoginButton();
210 console.log("Login state did not match, not continuing with login.");
214 function displayLogin() {
215 document.getElementById("loginbtn").classList.add("hidden");
216 document.getElementById("logindesc").classList.add("hidden");
217 document.getElementById("username").classList.remove("hidden");
218 document.getElementById("username").textContent = userData.email;
219 document.getElementById("uploadTrackButton").disabled = false;
220 document.getElementById("libraryShowLine").classList.remove("hidden");
221 document.getElementById("logoutbtn").classList.remove("hidden");
224 function displayLogout() {
225 document.getElementById("logoutbtn").classList.add("hidden");
226 document.getElementById("username").classList.add("hidden");
227 document.getElementById("username").textContent = "";
228 document.getElementById("uploadTrackButton").disabled = true;
229 document.getElementById("libraryShowLine").classList.add("hidden");
230 document.getElementById("loginbtn").classList.remove("hidden");
231 document.getElementById("logindesc").classList.remove("hidden");
234 function initDB(aEvent) {
237 gAction.removeEventListener(aEvent.type, initDB, false);
238 var request = window.indexedDB.open("MainDB-lantea", 2);
239 request.onerror = function(event) {
240 // Errors can be handled here. Error codes explain in:
241 // https://developer.mozilla.org/en/IndexedDB/IDBDatabaseException#Constants
242 console.log("error opening mainDB: " + event.target.error);
245 console.log("error code: " + event.target.error.code +
246 " - name: " + event.target.error.name);
249 request.onsuccess = function(event) {
250 mainDB = event.target.result;
251 var throwEv = new CustomEvent("dbinit-done");
252 gAction.dispatchEvent(throwEv);
254 request.onupgradeneeded = function(event) {
255 mainDB = request.result;
256 var ver = mainDB.version || 0; // version is empty string for a new DB
258 console.log("mainDB has version " + ver + ", upgrade needed.");
259 if (!mainDB.objectStoreNames.contains("prefs")) {
260 // Create a "prefs" objectStore.
261 var prefsStore = mainDB.createObjectStore("prefs");
264 if (!mainDB.objectStoreNames.contains("track")) {
265 // Create a "track" objectStore.
266 var trackStore = mainDB.createObjectStore("track", {autoIncrement: true});
268 if (!mainDB.objectStoreNames.contains("tilecache")) {
269 // Create a "tilecache" objectStore.
270 var tilecacheStore = mainDB.createObjectStore("tilecache");
272 mainDB.onversionchange = function(event) {
281 if (gUIHideCountdown <= 0) {
282 var areas = document.getElementsByClassName('autoFade');
283 for (var i = 0; i <= areas.length - 1; i++) {
284 areas[i].classList.remove("hidden");
286 setTimeout(maybeHideUI, 1000);
288 gUIHideCountdown = 5;
291 function maybeHideUI() {
293 if (gUIHideCountdown <= 0) {
294 var areas = document.getElementsByClassName('autoFade');
295 for (var i = 0; i <= areas.length - 1; i++) {
296 areas[i].classList.add("hidden");
300 setTimeout(maybeHideUI, 1000);
304 function updateTrackInfo() {
305 document.getElementById("trackLengthNum").textContent = calcTrackLength().toFixed(1);
306 var duration = calcTrackDuration();
307 var durationM = Math.round(duration/60);
308 var durationH = Math.floor(durationM/60); durationM = durationM - durationH * 60;
309 document.getElementById("trackDurationH").style.display = durationH ? "inline" : "none";
310 document.getElementById("trackDurationHNum").textContent = durationH;
311 document.getElementById("trackDurationMNum").textContent = durationM;
314 function toggleTrackArea() {
315 var fs = document.getElementById("trackArea");
316 if (fs.classList.contains("hidden")) {
317 prepareLoginButton();
318 fs.classList.remove("hidden");
320 gTrackUpdateInterval = setInterval(updateTrackInfo, 1000);
323 clearInterval(gTrackUpdateInterval);
324 fs.classList.add("hidden");
328 function toggleSettings() {
329 var fs = document.getElementById("settingsArea");
330 if (fs.classList.contains("hidden")) {
331 fs.classList.remove("hidden");
335 fs.classList.add("hidden");
339 function toggleFullscreen() {
340 if ((document.fullScreenElement && document.fullScreenElement !== null) ||
341 (document.mozFullScreenElement && document.mozFullScreenElement !== null) ||
342 (document.webkitFullScreenElement && document.webkitFullScreenElement !== null)) {
343 if (document.cancelFullScreen) {
344 document.cancelFullScreen();
345 } else if (document.mozCancelFullScreen) {
346 document.mozCancelFullScreen();
347 } else if (document.webkitCancelFullScreen) {
348 document.webkitCancelFullScreen();
352 var elem = document.getElementById("body");
353 if (elem.requestFullScreen) {
354 elem.requestFullScreen();
355 } else if (elem.mozRequestFullScreen) {
356 elem.mozRequestFullScreen();
357 } else if (elem.webkitRequestFullScreen) {
358 elem.webkitRequestFullScreen();
363 function showUploadDialog() {
364 var dia = document.getElementById("trackDialogArea");
365 var areas = dia.children;
366 for (var i = 0; i <= areas.length - 1; i++) {
367 areas[i].style.display = "none";
369 document.getElementById("uploadDialog").style.display = "block";
370 document.getElementById("uploadTrackButton").disabled = true;
371 dia.classList.remove("hidden");
374 function cancelTrackDialog() {
375 document.getElementById("trackDialogArea").classList.add("hidden");
376 document.getElementById("uploadTrackButton").disabled = false;
379 function showGLWarningDialog() {
380 var dia = document.getElementById("dialogArea");
381 var areas = dia.children;
382 for (var i = 0; i <= areas.length - 1; i++) {
383 areas[i].style.display = "none";
385 document.getElementById("noGLwarning").style.display = "block";
386 dia.classList.remove("hidden");
389 function showDBErrorDialog() {
390 var dia = document.getElementById("dialogArea");
391 var areas = dia.children;
392 for (var i = 0; i <= areas.length - 1; i++) {
393 areas[i].style.display = "none";
395 document.getElementById("DBError").style.display = "block";
396 dia.classList.remove("hidden");
399 function showFirstRunDialog() {
400 var dia = document.getElementById("dialogArea");
401 var areas = dia.children;
402 for (var i = 0; i <= areas.length - 1; i++) {
403 areas[i].style.display = "none";
405 document.getElementById("firstRunIntro").style.display = "block";
406 dia.classList.remove("hidden");
409 function closeDialog() {
410 document.getElementById("dialogArea").classList.add("hidden");
413 function showInfoDialog() {
414 var dia = document.getElementById("dialogArea");
415 var areas = dia.children;
416 for (var i = 0; i <= areas.length - 1; i++) {
417 areas[i].style.display = "none";
419 document.getElementById("infoDialog").style.display = "block";
420 dia.classList.remove("hidden");
424 handleEvent: function(aEvent) {
425 var touchEvent = aEvent.type.indexOf('touch') != -1;
427 switch (aEvent.type) {
441 function setUploadField(aField) {
443 case "uploadDevName":
444 gPrefs.set("devicename", aField.value);
449 function makeISOString(aTimestamp) {
450 // ISO time format is YYYY-MM-DDTHH:mm:ssZ
451 var tsDate = new Date(aTimestamp);
452 // Note that .getUTCMonth() returns a number between 0 and 11 (0 for January)!
453 return tsDate.getUTCFullYear() + "-" +
454 (tsDate.getUTCMonth() < 9 ? "0" : "") + (tsDate.getUTCMonth() + 1 ) + "-" +
455 (tsDate.getUTCDate() < 10 ? "0" : "") + tsDate.getUTCDate() + "T" +
456 (tsDate.getUTCHours() < 10 ? "0" : "") + tsDate.getUTCHours() + ":" +
457 (tsDate.getUTCMinutes() < 10 ? "0" : "") + tsDate.getUTCMinutes() + ":" +
458 (tsDate.getUTCSeconds() < 10 ? "0" : "") + tsDate.getUTCSeconds() + "Z";
461 function convertTrack(aTargetFormat) {
463 switch (aTargetFormat) {
465 out += '<?xml version="1.0" encoding="UTF-8" ?>' + "\n\n";
466 out += '<gpx version="1.0" creator="Lantea" xmlns="http://www.topografix.com/GPX/1/0">' + "\n";
468 out += ' <trk>' + "\n";
469 out += ' <trkseg>' + "\n";
470 for (var i = 0; i < gTrack.length; i++) {
471 if (gTrack[i].beginSegment && i > 0) {
472 out += ' </trkseg>' + "\n";
473 out += ' <trkseg>' + "\n";
475 out += ' <trkpt lat="' + gTrack[i].coords.latitude + '" lon="' +
476 gTrack[i].coords.longitude + '">' + "\n";
477 if (gTrack[i].coords.altitude) {
478 out += ' <ele>' + gTrack[i].coords.altitude + '</ele>' + "\n";
480 out += ' <time>' + makeISOString(gTrack[i].time) + '</time>' + "\n";
481 out += ' </trkpt>' + "\n";
483 out += ' </trkseg>' + "\n";
484 out += ' </trk>' + "\n";
486 out += '</gpx>' + "\n";
489 out = JSON.stringify(gTrack);
497 function saveTrack() {
499 var outDataURI = "data:application/gpx+xml," +
500 encodeURIComponent(convertTrack("gpx"));
501 window.open(outDataURI, 'GPX Track');
505 function saveTrackDump() {
507 var outDataURI = "data:application/json," +
508 encodeURIComponent(convertTrack("json"));
509 window.open(outDataURI, 'JSON dump');
513 function uploadTrack() {
514 // Hide all areas in the dialog.
515 var dia = document.getElementById("trackDialogArea");
516 var areas = dia.children;
517 for (var i = 0; i <= areas.length - 1; i++) {
518 areas[i].style.display = "none";
520 // Reset all the fields in the status area.
521 document.getElementById("uploadStatusCloseButton").disabled = true;
522 document.getElementById("uploadInProgress").style.display = "block";
523 document.getElementById("uploadSuccess").style.display = "none";
524 document.getElementById("uploadFailed").style.display = "none";
525 document.getElementById("uploadError").style.display = "none";
526 document.getElementById("uploadErrorMsg").textContent = "";
527 // Now show the status area.
528 document.getElementById("uploadStatus").style.display = "block";
530 // Assemble field to post to the backend.
531 var formData = new FormData();
532 formData.append("jsondata", convertTrack("json"));
533 var desc = document.getElementById("uploadDesc").value;
534 formData.append("comment",
535 desc.length ? desc : "Track recorded via Lantea Maps");
536 formData.append("devicename",
537 document.getElementById("uploadDevName").value);
538 formData.append("public",
539 document.getElementById("uploadPublic").value);
541 fetchBackend("save_track", "POST", formData,
542 function(aResult, aStatusCode) {
543 if (aStatusCode >= 400) {
544 reportUploadStatus(false, aResult);
546 else if (aResult["id"]) {
547 reportUploadStatus(true);
549 else { // If no ID is returned, we assume a general error.
550 reportUploadStatus(false);
556 function reportUploadStatus(aSuccess, aResponse) {
557 document.getElementById("uploadStatusCloseButton").disabled = false;
558 document.getElementById("uploadInProgress").style.display = "none";
560 document.getElementById("uploadSuccess").style.display = "block";
562 else if (aResponse && aResponse["message"]) {
563 document.getElementById("uploadErrorMsg").textContent = aResponse["message"];
564 if (aResponse["errortype"]) {
565 document.getElementById("uploadErrorMsg").textContent += " (" + aResponse["errortype"] + ")";
567 document.getElementById("uploadError").style.display = "block";
569 else if (aResponse) {
570 document.getElementById("uploadErrorMsg").textContent = aResponse;
571 document.getElementById("uploadError").style.display = "block";
574 document.getElementById("uploadFailed").style.display = "block";
581 get: function(aKey, aCallback) {
584 var transaction = mainDB.transaction([this.objStore]);
585 var request = transaction.objectStore(this.objStore).get(aKey);
586 request.onsuccess = function(event) {
587 aCallback(request.result, event);
589 request.onerror = function(event) {
590 // Errors can be handled here.
591 aCallback(undefined, event);
595 set: function(aKey, aValue, aCallback) {
599 var transaction = mainDB.transaction([this.objStore], "readwrite");
600 var objStore = transaction.objectStore(this.objStore);
601 var request = objStore.put(aValue, aKey);
602 request.onsuccess = function(event) {
605 aCallback(success, event);
607 request.onerror = function(event) {
608 // Errors can be handled here.
610 aCallback(success, event);
614 unset: function(aKey, aCallback) {
618 var transaction = mainDB.transaction([this.objStore], "readwrite");
619 var request = transaction.objectStore(this.objStore).delete(aKey);
620 request.onsuccess = function(event) {
623 aCallback(success, event);
625 request.onerror = function(event) {
626 // Errors can be handled here.
628 aCallback(success, event);
636 getList: function(aCallback) {
639 var transaction = mainDB.transaction([this.objStore]);
640 var objStore = transaction.objectStore(this.objStore);
641 if (objStore.getAll) { // currently Mozilla-specific
642 objStore.getAll().onsuccess = function(event) {
643 aCallback(event.target.result);
646 else { // Use cursor (standard method).
648 objStore.openCursor().onsuccess = function(event) {
649 var cursor = event.target.result;
651 tPoints.push(cursor.value);
661 getListStepped: function(aCallback) {
664 var transaction = mainDB.transaction([this.objStore]);
665 var objStore = transaction.objectStore(this.objStore);
666 // Use cursor in reverse direction (so we get the most recent position first)
667 objStore.openCursor(null, "prev").onsuccess = function(event) {
668 var cursor = event.target.result;
670 aCallback(cursor.value);
679 push: function(aValue, aCallback) {
682 var transaction = mainDB.transaction([this.objStore], "readwrite");
683 var objStore = transaction.objectStore(this.objStore);
684 var request = objStore.add(aValue);
685 request.onsuccess = function(event) {
687 aCallback(request.result, event);
689 request.onerror = function(event) {
690 // Errors can be handled here.
692 aCallback(false, event);
696 clear: function(aCallback) {
700 var transaction = mainDB.transaction([this.objStore], "readwrite");
701 var request = transaction.objectStore(this.objStore).clear();
702 request.onsuccess = function(event) {
705 aCallback(success, event);
707 request.onerror = function(event) {
708 // Errors can be handled here.
710 aCallback(success, event);
715 function fetchBackend(aEndpoint, aMethod, aSendData, aCallback, aCallbackForwards) {
716 var XHR = new XMLHttpRequest();
717 XHR.onreadystatechange = function() {
718 if (XHR.readyState == 4) {
719 // State says we are fully loaded.
721 if (XHR.getResponseHeader("Content-Type") == "application/json") {
722 // Got a JSON object, see if we have success.
724 result = JSON.parse(XHR.responseText);
728 result = {"error": e,
729 "message": XHR.responseText};
733 result = XHR.responseText;
735 aCallback(result, XHR.status, aCallbackForwards);
738 XHR.open(aMethod, gBackendURL + aEndpoint, true);
739 XHR.withCredentials = "true";
740 //XHR.setRequestHeader("Accept", "application/json");
742 XHR.send(aSendData); // Send actual form data.
745 aCallback(e, 500, aCallbackForwards);