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;
11 var gUIHideCountdown = 0;
13 var gTrackUpdateInterval;
14 var gAction, gActionLabel;
15 var gBackendURL = "https://backend.lantea.kairo.at";
16 var gAuthClientID = "lantea";
18 window.onload = function() {
19 if (/\/login\.html/.test(window.location)) {
20 // If we are in the login window, call a function to complete the process and don't do anything else here.
21 completeLoginWindow();
24 gAction = document.getElementById("action");
25 gActionLabel = document.getElementById("actionlabel");
27 var mSel = document.getElementById("mapSelector");
28 for (var mapStyle in gMapStyles) {
29 var opt = document.createElement("option");
31 opt.text = gMapStyles[mapStyle].name;
35 var areas = document.getElementsByClassName("overlayArea");
36 for (var i = 0; i <= areas.length - 1; i++) {
37 areas[i].addEventListener("mouseup", uiEvHandler, false);
38 areas[i].addEventListener("mousemove", uiEvHandler, false);
39 areas[i].addEventListener("mousedown", uiEvHandler, false);
40 areas[i].addEventListener("mouseout", uiEvHandler, false);
42 areas[i].addEventListener("touchstart", uiEvHandler, false);
43 areas[i].addEventListener("touchmove", uiEvHandler, false);
44 areas[i].addEventListener("touchend", uiEvHandler, false);
45 areas[i].addEventListener("touchcancel", uiEvHandler, false);
46 areas[i].addEventListener("touchleave", uiEvHandler, false);
49 document.getElementById("body").addEventListener("keydown", uiEvHandler, false);
51 if (navigator.platform.length == "") {
52 // For Firefox OS, don't display the "save" button.
53 // Do this by setting the debugHide class for testing in debug mode.
54 document.getElementById("saveTrackButton").classList.add("debugHide");
57 // Set backend URL in a way that it works for testing on localhost as well as
58 // both the lantea.kairo.at and lantea-dev.kairo.at deployments.
59 if (window.location.host == "localhost") {
60 gBackendURL = window.location.protocol + '//' + window.location.host + "/lantea-backend/";
63 gBackendURL = window.location.protocol + '//' + "backend." + window.location.host + "/";
65 // Make sure to use a different login client ID for the -dev setup.
66 if (/\-dev\./.test(window.location.host)) {
67 gAuthClientID += "-dev";
70 document.getElementById("libCloseButton").onclick = hideLibrary;
72 // Set up the login area.
73 document.getElementById("loginbtn").onclick = startLogin;
74 document.getElementById("logoutbtn").onclick = doLogout;
75 prepareLoginButton(function() {
76 // Anything that needs the backend should only be triggered from in here.
77 // That makes sure that the first call the the backend is oauth_state and no other is running in parallel.
78 // If we call multiple backend methods at once and no session is open, we create multiple sessions, which calls for confusion later on.
80 // Call any UI preparation that needs the backend.
83 gAction.addEventListener("dbinit-done", initMap, false);
84 gAction.addEventListener("mapinit-done", postInit, false);
85 console.log("starting DB init...");
89 function postInit(aEvent) {
90 gAction.removeEventListener(aEvent.type, postInit, false);
91 console.log("init done, draw map.");
92 gMapPrefsLoaded = true;
94 //gMap.resizeAndDraw(); <-- HACK: This triggers bug 1001853, work around with a delay.
95 window.setTimeout(gMap.resizeAndDraw, 100);
96 gActionLabel.textContent = "";
97 gAction.style.display = "none";
98 setTracking(document.getElementById("trackCheckbox"));
99 gPrefs.get("devicename", function(aValue) {
101 document.getElementById("uploadDevName").value = aValue;
106 window.onresize = function() {
107 gMap.resizeAndDraw();
110 function startLogin() {
111 var authURL = authData["url"] + "authorize?response_type=code&client_id=" + gAuthClientID + "&scope=email" +
112 "&state=" + authData["state"] + "&redirect_uri=" + encodeURIComponent(getRedirectURI());
113 if (window.open(authURL, "KaiRoAuth", 'height=450,width=600')) {
114 console.log("Sign In window open.");
117 console.log("Opening Sign In window failed.");
121 function getRedirectURI() {
122 return window.location.protocol + '//' + window.location.host + window.location.pathname.replace("index.html", "") + "login.html";
125 function doLogout() {
126 fetchBackend("logout", "GET", null,
127 function(aResult, aStatus) {
129 prepareLoginButton();
132 console.log("Backend issue trying to log out.");
139 function prepareLoginButton(aCallback) {
140 fetchBackend("oauth_state", "GET", null,
141 function(aResult, aStatus) {
142 if (aStatus == 200) {
143 if (aResult["logged_in"]) {
145 "email": aResult["email"],
146 "permissions": aResult["permissions"],
152 authData = {"state": aResult["state"], "url": aResult["url"]};
158 console.log("Backend error " + aStatus + " fetching OAuth state: " + aResult["message"]);
160 if (aCallback) { aCallback(); }
166 function completeLoginWindow() {
168 window.opener.finishLogin(getParameterByName("code"), getParameterByName("state"));
172 document.getElementById("logininfo").textContent = "You have called this document outside of the login flow, which is not supported.";
176 function finishLogin(aCode, aState) {
177 if (aState == authData["state"]) {
178 fetchBackend("login?code=" + aCode + "&state=" + aState + "&redirect_uri=" + encodeURIComponent(getRedirectURI()), "GET", null,
179 function(aResult, aStatus) {
180 if (aStatus == 200) {
182 "email": aResult["email"],
183 "permissions": aResult["permissions"],
188 console.log("Login error " + aStatus + ": " + aResult["message"]);
189 prepareLoginButton();
196 console.log("Login state did not match, not continuing with login.");
200 function displayLogin() {
201 document.getElementById("loginbtn").classList.add("hidden");
202 document.getElementById("logindesc").classList.add("hidden");
203 document.getElementById("username").classList.remove("hidden");
204 document.getElementById("username").textContent = userData.email;
205 document.getElementById("uploadTrackButton").disabled = false;
206 document.getElementById("libraryShowLine").classList.remove("hidden");
207 document.getElementById("logoutbtn").classList.remove("hidden");
210 function displayLogout() {
211 document.getElementById("logoutbtn").classList.add("hidden");
212 document.getElementById("username").classList.add("hidden");
213 document.getElementById("username").textContent = "";
214 document.getElementById("uploadTrackButton").disabled = true;
215 document.getElementById("libraryShowLine").classList.add("hidden");
216 document.getElementById("loginbtn").classList.remove("hidden");
217 document.getElementById("logindesc").classList.remove("hidden");
220 function initDB(aEvent) {
223 gAction.removeEventListener(aEvent.type, initDB, false);
224 var request = window.indexedDB.open("MainDB-lantea", 2);
225 request.onerror = function(event) {
226 // Errors can be handled here. Error codes explain in:
227 // https://developer.mozilla.org/en/IndexedDB/IDBDatabaseException#Constants
229 console.log("error opening mainDB: " + event.target.errorCode);
231 request.onsuccess = function(event) {
232 mainDB = request.result;
233 var throwEv = new CustomEvent("dbinit-done");
234 gAction.dispatchEvent(throwEv);
236 request.onupgradeneeded = function(event) {
237 mainDB = request.result;
238 var ver = mainDB.version || 0; // version is empty string for a new DB
240 console.log("mainDB has version " + ver + ", upgrade needed.");
241 if (!mainDB.objectStoreNames.contains("prefs")) {
242 // Create a "prefs" objectStore.
243 var prefsStore = mainDB.createObjectStore("prefs");
245 if (!mainDB.objectStoreNames.contains("track")) {
246 // Create a "track" objectStore.
247 var trackStore = mainDB.createObjectStore("track", {autoIncrement: true});
249 if (!mainDB.objectStoreNames.contains("tilecache")) {
250 // Create a "tilecache" objectStore.
251 var tilecacheStore = mainDB.createObjectStore("tilecache");
253 mainDB.onversionchange = function(event) {
262 if (gUIHideCountdown <= 0) {
263 var areas = document.getElementsByClassName('overlayArea');
264 for (var i = 0; i <= areas.length - 1; i++) {
265 areas[i].classList.remove("hidden");
267 setTimeout(maybeHideUI, 1000);
269 gUIHideCountdown = 5;
272 function maybeHideUI() {
274 if (gUIHideCountdown <= 0) {
275 var areas = document.getElementsByClassName('overlayArea');
276 for (var i = 0; i <= areas.length - 1; i++) {
277 areas[i].classList.add("hidden");
281 setTimeout(maybeHideUI, 1000);
285 function updateTrackInfo() {
286 document.getElementById("trackLengthNum").textContent = calcTrackLength().toFixed(1);
287 var duration = calcTrackDuration();
288 var durationM = Math.round(duration/60);
289 var durationH = Math.floor(durationM/60); durationM = durationM - durationH * 60;
290 document.getElementById("trackDurationH").style.display = durationH ? "inline" : "none";
291 document.getElementById("trackDurationHNum").textContent = durationH;
292 document.getElementById("trackDurationMNum").textContent = durationM;
295 function toggleTrackArea() {
296 var fs = document.getElementById("trackArea");
297 if (fs.classList.contains("hidden")) {
298 fs.classList.remove("hidden");
300 gTrackUpdateInterval = setInterval(updateTrackInfo, 1000);
303 clearInterval(gTrackUpdateInterval);
304 fs.classList.add("hidden");
308 function toggleSettings() {
309 var fs = document.getElementById("settingsArea");
310 if (fs.classList.contains("hidden")) {
311 fs.classList.remove("hidden");
315 fs.classList.add("hidden");
319 function toggleFullscreen() {
320 if ((document.fullScreenElement && document.fullScreenElement !== null) ||
321 (document.mozFullScreenElement && document.mozFullScreenElement !== null) ||
322 (document.webkitFullScreenElement && document.webkitFullScreenElement !== null)) {
323 if (document.cancelFullScreen) {
324 document.cancelFullScreen();
325 } else if (document.mozCancelFullScreen) {
326 document.mozCancelFullScreen();
327 } else if (document.webkitCancelFullScreen) {
328 document.webkitCancelFullScreen();
332 var elem = document.getElementById("body");
333 if (elem.requestFullScreen) {
334 elem.requestFullScreen();
335 } else if (elem.mozRequestFullScreen) {
336 elem.mozRequestFullScreen();
337 } else if (elem.webkitRequestFullScreen) {
338 elem.webkitRequestFullScreen();
343 function showUploadDialog() {
344 var dia = document.getElementById("trackDialogArea");
345 var areas = dia.children;
346 for (var i = 0; i <= areas.length - 1; i++) {
347 areas[i].style.display = "none";
349 document.getElementById("uploadDialog").style.display = "block";
350 document.getElementById("uploadTrackButton").disabled = true;
351 dia.classList.remove("hidden");
354 function showGLWarningDialog() {
355 var dia = document.getElementById("dialogArea");
356 var areas = dia.children;
357 for (var i = 0; i <= areas.length - 1; i++) {
358 areas[i].style.display = "none";
360 document.getElementById("noGLwarning").style.display = "block";
361 dia.classList.remove("hidden");
364 function cancelTrackDialog() {
365 document.getElementById("trackDialogArea").classList.add("hidden");
366 document.getElementById("uploadTrackButton").disabled = false;
370 handleEvent: function(aEvent) {
371 var touchEvent = aEvent.type.indexOf('touch') != -1;
373 switch (aEvent.type) {
387 function setUploadField(aField) {
389 case "uploadDevName":
390 gPrefs.set("devicename", aField.value);
395 function makeISOString(aTimestamp) {
396 // ISO time format is YYYY-MM-DDTHH:mm:ssZ
397 var tsDate = new Date(aTimestamp);
398 // Note that .getUTCMonth() returns a number between 0 and 11 (0 for January)!
399 return tsDate.getUTCFullYear() + "-" +
400 (tsDate.getUTCMonth() < 9 ? "0" : "") + (tsDate.getUTCMonth() + 1 ) + "-" +
401 (tsDate.getUTCDate() < 10 ? "0" : "") + tsDate.getUTCDate() + "T" +
402 (tsDate.getUTCHours() < 10 ? "0" : "") + tsDate.getUTCHours() + ":" +
403 (tsDate.getUTCMinutes() < 10 ? "0" : "") + tsDate.getUTCMinutes() + ":" +
404 (tsDate.getUTCSeconds() < 10 ? "0" : "") + tsDate.getUTCSeconds() + "Z";
407 function convertTrack(aTargetFormat) {
409 switch (aTargetFormat) {
411 out += '<?xml version="1.0" encoding="UTF-8" ?>' + "\n\n";
412 out += '<gpx version="1.0" creator="Lantea" xmlns="http://www.topografix.com/GPX/1/0">' + "\n";
414 out += ' <trk>' + "\n";
415 out += ' <trkseg>' + "\n";
416 for (var i = 0; i < gTrack.length; i++) {
417 if (gTrack[i].beginSegment && i > 0) {
418 out += ' </trkseg>' + "\n";
419 out += ' <trkseg>' + "\n";
421 out += ' <trkpt lat="' + gTrack[i].coords.latitude + '" lon="' +
422 gTrack[i].coords.longitude + '">' + "\n";
423 if (gTrack[i].coords.altitude) {
424 out += ' <ele>' + gTrack[i].coords.altitude + '</ele>' + "\n";
426 out += ' <time>' + makeISOString(gTrack[i].time) + '</time>' + "\n";
427 out += ' </trkpt>' + "\n";
429 out += ' </trkseg>' + "\n";
430 out += ' </trk>' + "\n";
432 out += '</gpx>' + "\n";
435 out = JSON.stringify(gTrack);
443 function saveTrack() {
445 var outDataURI = "data:application/gpx+xml," +
446 encodeURIComponent(convertTrack("gpx"));
447 window.open(outDataURI, 'GPX Track');
451 function saveTrackDump() {
453 var outDataURI = "data:application/json," +
454 encodeURIComponent(convertTrack("json"));
455 window.open(outDataURI, 'JSON dump');
459 function uploadTrack() {
460 // Hide all areas in the dialog.
461 var dia = document.getElementById("trackDialogArea");
462 var areas = dia.children;
463 for (var i = 0; i <= areas.length - 1; i++) {
464 areas[i].style.display = "none";
466 // Reset all the fields in the status area.
467 document.getElementById("uploadStatusCloseButton").disabled = true;
468 document.getElementById("uploadInProgress").style.display = "block";
469 document.getElementById("uploadSuccess").style.display = "none";
470 document.getElementById("uploadFailed").style.display = "none";
471 document.getElementById("uploadError").style.display = "none";
472 document.getElementById("uploadErrorMsg").textContent = "";
473 // Now show the status area.
474 document.getElementById("uploadStatus").style.display = "block";
476 // Assemble field to post to the backend.
477 var formData = new FormData();
478 formData.append("jsondata", convertTrack("json"));
479 var desc = document.getElementById("uploadDesc").value;
480 formData.append("comment",
481 desc.length ? desc : "Track recorded via Lantea Maps");
482 formData.append("devicename",
483 document.getElementById("uploadDevName").value);
484 formData.append("public",
485 document.getElementById("uploadPublic").value);
487 fetchBackend("save_track", "POST", formData,
488 function(aResult, aStatusCode) {
489 if (aStatusCode >= 400) {
490 reportUploadStatus(false, aResult);
493 reportUploadStatus(true);
499 function reportUploadStatus(aSuccess, aMessage) {
500 document.getElementById("uploadStatusCloseButton").disabled = false;
501 document.getElementById("uploadInProgress").style.display = "none";
503 document.getElementById("uploadSuccess").style.display = "block";
506 document.getElementById("uploadErrorMsg").textContent = aMessage;
507 document.getElementById("uploadError").style.display = "block";
510 document.getElementById("uploadFailed").style.display = "block";
517 get: function(aKey, aCallback) {
520 var transaction = mainDB.transaction([this.objStore]);
521 var request = transaction.objectStore(this.objStore).get(aKey);
522 request.onsuccess = function(event) {
523 aCallback(request.result, event);
525 request.onerror = function(event) {
526 // Errors can be handled here.
527 aCallback(undefined, event);
531 set: function(aKey, aValue, aCallback) {
535 var transaction = mainDB.transaction([this.objStore], "readwrite");
536 var objStore = transaction.objectStore(this.objStore);
537 var request = objStore.put(aValue, aKey);
538 request.onsuccess = function(event) {
541 aCallback(success, event);
543 request.onerror = function(event) {
544 // Errors can be handled here.
546 aCallback(success, event);
550 unset: function(aKey, aCallback) {
554 var transaction = mainDB.transaction([this.objStore], "readwrite");
555 var request = transaction.objectStore(this.objStore).delete(aKey);
556 request.onsuccess = function(event) {
559 aCallback(success, event);
561 request.onerror = function(event) {
562 // Errors can be handled here.
564 aCallback(success, event);
572 getList: function(aCallback) {
575 var transaction = mainDB.transaction([this.objStore]);
576 var objStore = transaction.objectStore(this.objStore);
577 if (objStore.getAll) { // currently Mozilla-specific
578 objStore.getAll().onsuccess = function(event) {
579 aCallback(event.target.result);
582 else { // Use cursor (standard method).
584 objStore.openCursor().onsuccess = function(event) {
585 var cursor = event.target.result;
587 tPoints.push(cursor.value);
597 getListStepped: function(aCallback) {
600 var transaction = mainDB.transaction([this.objStore]);
601 var objStore = transaction.objectStore(this.objStore);
602 // Use cursor in reverse direction (so we get the most recent position first)
603 objStore.openCursor(null, "prev").onsuccess = function(event) {
604 var cursor = event.target.result;
606 aCallback(cursor.value);
615 push: function(aValue, aCallback) {
618 var transaction = mainDB.transaction([this.objStore], "readwrite");
619 var objStore = transaction.objectStore(this.objStore);
620 var request = objStore.add(aValue);
621 request.onsuccess = function(event) {
623 aCallback(request.result, event);
625 request.onerror = function(event) {
626 // Errors can be handled here.
628 aCallback(false, event);
632 clear: function(aCallback) {
636 var transaction = mainDB.transaction([this.objStore], "readwrite");
637 var request = transaction.objectStore(this.objStore).clear();
638 request.onsuccess = function(event) {
641 aCallback(success, event);
643 request.onerror = function(event) {
644 // Errors can be handled here.
646 aCallback(success, event);
651 function fetchBackend(aEndpoint, aMethod, aSendData, aCallback, aCallbackForwards) {
652 var XHR = new XMLHttpRequest();
653 XHR.onreadystatechange = function() {
654 if (XHR.readyState == 4) {
655 // State says we are fully loaded.
657 if (XHR.getResponseHeader("Content-Type") == "application/json") {
658 // Got a JSON object, see if we have success.
660 result = JSON.parse(XHR.responseText);
664 result = {"error": e,
665 "message": XHR.responseText};
669 result = XHR.responseText;
671 aCallback(result, XHR.status, aCallbackForwards);
674 XHR.open(aMethod, gBackendURL + aEndpoint, true);
675 XHR.withCredentials = "true";
676 //XHR.setRequestHeader("Accept", "application/json");
678 XHR.send(aSendData); // Send actual form data.
681 aCallback(e, 500, aCallbackForwards);
685 function getParameterByName(aName) {
686 // from http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
687 name = aName.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
688 var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
689 results = regex.exec(location.search);
690 return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));