remove debug statements as it works now
[lantea.git] / js / ui.js
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/. */
4
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;
8
9 var mainDB;
10 var gAppInitDone = false;
11 var firstRun = false;
12 var gUIHideCountdown = 0;
13 var gWaitCounter = 0;
14 var gTrackUpdateInterval;
15 var gAction, gActionLabel;
16 var gBackendURL = "https://backend.lantea.kairo.at";
17 var gAuthClientID = "lantea";
18
19 window.onload = function() {
20   gAction = document.getElementById("action");
21   gActionLabel = document.getElementById("actionlabel");
22
23   var mSel = document.getElementById("mapSelector");
24   for (var mapStyle in gMapStyles) {
25     var opt = document.createElement("option");
26     opt.value = mapStyle;
27     opt.text = gMapStyles[mapStyle].name;
28     mSel.add(opt, null);
29   }
30
31   var areas = document.getElementsByClassName("autoFade");
32   for (var i = 0; i <= areas.length - 1; i++) {
33     areas[i].addEventListener("mouseup", uiEvHandler, false);
34     areas[i].addEventListener("mousemove", uiEvHandler, false);
35     areas[i].addEventListener("mousedown", uiEvHandler, false);
36     areas[i].addEventListener("mouseout", uiEvHandler, false);
37
38     areas[i].addEventListener("touchstart", uiEvHandler, false);
39     areas[i].addEventListener("touchmove", uiEvHandler, false);
40     areas[i].addEventListener("touchend", uiEvHandler, false);
41     areas[i].addEventListener("touchcancel", uiEvHandler, false);
42     areas[i].addEventListener("touchleave", uiEvHandler, false);
43   }
44
45   document.getElementById("body").addEventListener("keydown", uiEvHandler, false);
46
47   if (navigator.platform.length == "") {
48     // For Firefox OS, don't display the "save" button.
49     // Do this by setting the debugHide class for testing in debug mode.
50     document.getElementById("saveTrackButton").classList.add("debugHide");
51   }
52
53   // Set backend URL in a way that it works for testing on localhost as well as
54   // both the lantea.kairo.at and lantea-dev.kairo.at deployments.
55   if (window.location.host == "localhost") {
56     gBackendURL = window.location.protocol + '//' + window.location.host + "/lantea-backend/";
57   }
58   else {
59     gBackendURL = window.location.protocol + '//' + "backend." + window.location.host + "/";
60   }
61   // Make sure to use a different login client ID for the -dev setup.
62   if (/\-dev\./.test(window.location.host)) {
63     gAuthClientID += "-dev";
64   }
65
66   document.getElementById("libCloseButton").onclick = hideLibrary;
67
68   // Set up the login area.
69   document.getElementById("loginbtn").onclick = startLogin;
70   document.getElementById("logoutbtn").onclick = doLogout;
71   prepareLoginButton(function() {
72     // Anything that needs the backend should only be triggered from in here.
73     // That makes sure that the first call the the backend is oauth_state and no other is running in parallel.
74     // If we call multiple backend methods at once and no session is open, we create multiple sessions, which calls for confusion later on.
75
76     // Call any UI preparation that needs the backend.
77   });
78
79   gAction.addEventListener("dbinit-done", initMap, false);
80   gAction.addEventListener("mapinit-done", postInit, false);
81   console.log("starting DB init...");
82   initDB();
83 }
84
85 function postInit(aEvent) {
86   gAction.removeEventListener(aEvent.type, postInit, false);
87   console.log("init done, draw map.");
88   gMapPrefsLoaded = true;
89   gAppInitDone = true;
90   //gMap.resizeAndDraw();  <-- HACK: This triggers bug 1001853, work around with a delay.
91   window.setTimeout(gMap.resizeAndDraw, 100);
92   gActionLabel.textContent = "";
93   gAction.style.display = "none";
94   setTracking(document.getElementById("trackCheckbox"));
95   gPrefs.get("devicename", function(aValue) {
96     if (aValue) {
97       document.getElementById("uploadDevName").value = aValue;
98     }
99   });
100   if (firstRun) {
101     showFirstRunDialog();
102   }
103   else {
104     gPrefs.get("lastInfoShown", function(aValue) {
105       if (!aValue || !parseInt(aValue) || parseInt(aValue) < 1) {
106         showInfoDialog();
107       }
108     });
109   }
110   gPrefs.set("lastInfoShown", 1);
111 }
112
113 window.onresize = function() {
114   gMap.resizeAndDraw();
115 }
116
117 function startLogin() {
118   var authURL = authData["url"] + "authorize?response_type=code&client_id=" + gAuthClientID + "&scope=email" +
119                 "&state=" + authData["state"] + "&redirect_uri=" + encodeURIComponent(getRedirectURI());
120   if (window.open(authURL, "KaiRoAuth", 'height=450,width=600')) {
121     console.log("Sign In window open.");
122   }
123   else {
124     console.log("Opening Sign In window failed.");
125   }
126 }
127
128 function getRedirectURI() {
129   return window.location.protocol + '//' + window.location.host + window.location.pathname.replace("index.html", "") + "login.html";
130 }
131
132 function doLogout() {
133   fetchBackend("logout", "GET", null,
134      function(aResult, aStatus) {
135         if (aStatus < 400) {
136           prepareLoginButton();
137         }
138         else {
139           console.log("Backend issue trying to log out.");
140         }
141       },
142       {}
143   );
144 }
145
146 function prepareLoginButton(aCallback) {
147   fetchBackend("oauth_state", "GET", null,
148       function(aResult, aStatus) {
149         if (aStatus == 200) {
150           if (aResult["logged_in"]) {
151             userData = {
152               "email": aResult["email"],
153               "permissions": aResult["permissions"],
154             };
155             authData = null;
156             displayLogin();
157           }
158           else {
159             authData = {"state": aResult["state"], "url": aResult["url"]};
160             userData = null;
161             displayLogout();
162           }
163         }
164         else {
165           console.log("Backend error " + aStatus + " fetching OAuth state: " + aResult["message"]);
166         }
167         if (aCallback) { aCallback(); }
168       },
169       {}
170   );
171 }
172
173 function finishLogin(aCode, aState) {
174   if (aState == authData["state"]) {
175     fetchBackend("login?code=" + aCode + "&state=" + aState + "&redirect_uri=" + encodeURIComponent(getRedirectURI()), "GET", null,
176         function(aResult, aStatus) {
177           if (aStatus == 200) {
178             userData = {
179               "email": aResult["email"],
180               "permissions": aResult["permissions"],
181             };
182             displayLogin();
183           }
184           else {
185             console.log("Login error " + aStatus + ": " + aResult["message"]);
186             prepareLoginButton();
187           }
188         },
189         {}
190     );
191   }
192   else {
193     console.log("Login state did not match, not continuing with login.");
194   }
195 }
196
197 function displayLogin() {
198   document.getElementById("loginbtn").classList.add("hidden");
199   document.getElementById("logindesc").classList.add("hidden");
200   document.getElementById("username").classList.remove("hidden");
201   document.getElementById("username").textContent = userData.email;
202   document.getElementById("uploadTrackButton").disabled = false;
203   document.getElementById("libraryShowLine").classList.remove("hidden");
204   document.getElementById("logoutbtn").classList.remove("hidden");
205 }
206
207 function displayLogout() {
208   document.getElementById("logoutbtn").classList.add("hidden");
209   document.getElementById("username").classList.add("hidden");
210   document.getElementById("username").textContent = "";
211   document.getElementById("uploadTrackButton").disabled = true;
212   document.getElementById("libraryShowLine").classList.add("hidden");
213   document.getElementById("loginbtn").classList.remove("hidden");
214   document.getElementById("logindesc").classList.remove("hidden");
215 }
216
217 function initDB(aEvent) {
218   // Open DB.
219   if (aEvent)
220     gAction.removeEventListener(aEvent.type, initDB, false);
221   var request = window.indexedDB.open("MainDB-lantea", 2);
222   request.onerror = function(event) {
223     // Errors can be handled here. Error codes explain in:
224     // https://developer.mozilla.org/en/IndexedDB/IDBDatabaseException#Constants
225     if (gDebug)
226       console.log("error opening mainDB: " + event.target.errorCode);
227   };
228   request.onsuccess = function(event) {
229     mainDB = request.result;
230     var throwEv = new CustomEvent("dbinit-done");
231     gAction.dispatchEvent(throwEv);
232   };
233   request.onupgradeneeded = function(event) {
234     mainDB = request.result;
235     var ver = mainDB.version || 0; // version is empty string for a new DB
236     if (gDebug)
237       console.log("mainDB has version " + ver + ", upgrade needed.");
238     if (!mainDB.objectStoreNames.contains("prefs")) {
239       // Create a "prefs" objectStore.
240       var prefsStore = mainDB.createObjectStore("prefs");
241       firstRun = true;
242     }
243     if (!mainDB.objectStoreNames.contains("track")) {
244       // Create a "track" objectStore.
245       var trackStore = mainDB.createObjectStore("track", {autoIncrement: true});
246     }
247     if (!mainDB.objectStoreNames.contains("tilecache")) {
248       // Create a "tilecache" objectStore.
249       var tilecacheStore = mainDB.createObjectStore("tilecache");
250     }
251     mainDB.onversionchange = function(event) {
252       mainDB.close();
253       mainDB = undefined;
254       initDB();
255     };
256   };
257 }
258
259 function showUI() {
260   if (gUIHideCountdown <= 0) {
261     var areas = document.getElementsByClassName('autoFade');
262     for (var i = 0; i <= areas.length - 1; i++) {
263       areas[i].classList.remove("hidden");
264     }
265     setTimeout(maybeHideUI, 1000);
266   }
267   gUIHideCountdown = 5;
268 }
269
270 function maybeHideUI() {
271   gUIHideCountdown--;
272   if (gUIHideCountdown <= 0) {
273     var areas = document.getElementsByClassName('autoFade');
274     for (var i = 0; i <= areas.length - 1; i++) {
275       areas[i].classList.add("hidden");
276     }
277   }
278   else {
279     setTimeout(maybeHideUI, 1000);
280   }
281 }
282
283 function updateTrackInfo() {
284   document.getElementById("trackLengthNum").textContent = calcTrackLength().toFixed(1);
285   var duration = calcTrackDuration();
286   var durationM = Math.round(duration/60);
287   var durationH = Math.floor(durationM/60); durationM = durationM - durationH * 60;
288   document.getElementById("trackDurationH").style.display = durationH ? "inline" : "none";
289   document.getElementById("trackDurationHNum").textContent = durationH;
290   document.getElementById("trackDurationMNum").textContent = durationM;
291 }
292
293 function toggleTrackArea() {
294   var fs = document.getElementById("trackArea");
295   if (fs.classList.contains("hidden")) {
296     prepareLoginButton();
297     fs.classList.remove("hidden");
298     showUI();
299     gTrackUpdateInterval = setInterval(updateTrackInfo, 1000);
300   }
301   else {
302     clearInterval(gTrackUpdateInterval);
303     fs.classList.add("hidden");
304   }
305 }
306
307 function toggleSettings() {
308   var fs = document.getElementById("settingsArea");
309   if (fs.classList.contains("hidden")) {
310     fs.classList.remove("hidden");
311     showUI();
312   }
313   else {
314     fs.classList.add("hidden");
315   }
316 }
317
318 function toggleFullscreen() {
319   if ((document.fullScreenElement && document.fullScreenElement !== null) ||
320       (document.mozFullScreenElement && document.mozFullScreenElement !== null) ||
321       (document.webkitFullScreenElement && document.webkitFullScreenElement !== null)) {
322     if (document.cancelFullScreen) {
323       document.cancelFullScreen();
324     } else if (document.mozCancelFullScreen) {
325       document.mozCancelFullScreen();
326     } else if (document.webkitCancelFullScreen) {
327       document.webkitCancelFullScreen();
328     }
329   }
330   else {
331     var elem = document.getElementById("body");
332     if (elem.requestFullScreen) {
333       elem.requestFullScreen();
334     } else if (elem.mozRequestFullScreen) {
335       elem.mozRequestFullScreen();
336     } else if (elem.webkitRequestFullScreen) {
337       elem.webkitRequestFullScreen();
338     }
339   }
340 }
341
342 function showUploadDialog() {
343   var dia = document.getElementById("trackDialogArea");
344   var areas = dia.children;
345   for (var i = 0; i <= areas.length - 1; i++) {
346     areas[i].style.display = "none";
347   }
348   document.getElementById("uploadDialog").style.display = "block";
349   document.getElementById("uploadTrackButton").disabled = true;
350   dia.classList.remove("hidden");
351 }
352
353 function cancelTrackDialog() {
354   document.getElementById("trackDialogArea").classList.add("hidden");
355   document.getElementById("uploadTrackButton").disabled = false;
356 }
357
358 function showGLWarningDialog() {
359   var dia = document.getElementById("dialogArea");
360   var areas = dia.children;
361   for (var i = 0; i <= areas.length - 1; i++) {
362     areas[i].style.display = "none";
363   }
364   document.getElementById("noGLwarning").style.display = "block";
365   dia.classList.remove("hidden");
366 }
367
368 function showFirstRunDialog() {
369   var dia = document.getElementById("dialogArea");
370   var areas = dia.children;
371   for (var i = 0; i <= areas.length - 1; i++) {
372     areas[i].style.display = "none";
373   }
374   document.getElementById("firstRunIntro").style.display = "block";
375   dia.classList.remove("hidden");
376 }
377
378 function closeDialog() {
379   document.getElementById("dialogArea").classList.add("hidden");
380 }
381
382 function showInfoDialog() {
383   var dia = document.getElementById("dialogArea");
384   var areas = dia.children;
385   for (var i = 0; i <= areas.length - 1; i++) {
386     areas[i].style.display = "none";
387   }
388   document.getElementById("infoDialog").style.display = "block";
389   dia.classList.remove("hidden");
390 }
391
392 var uiEvHandler = {
393   handleEvent: function(aEvent) {
394     var touchEvent = aEvent.type.indexOf('touch') != -1;
395
396     switch (aEvent.type) {
397       case "mousedown":
398       case "touchstart":
399       case "mousemove":
400       case "touchmove":
401       case "mouseup":
402       case "touchend":
403       case "keydown":
404         showUI();
405         break;
406     }
407   }
408 };
409
410 function setUploadField(aField) {
411   switch (aField.id) {
412     case "uploadDevName":
413       gPrefs.set("devicename", aField.value);
414       break;
415   }
416 }
417
418 function makeISOString(aTimestamp) {
419   // ISO time format is YYYY-MM-DDTHH:mm:ssZ
420   var tsDate = new Date(aTimestamp);
421   // Note that .getUTCMonth() returns a number between 0 and 11 (0 for January)!
422   return tsDate.getUTCFullYear() + "-" +
423          (tsDate.getUTCMonth() < 9 ? "0" : "") + (tsDate.getUTCMonth() + 1 ) + "-" +
424          (tsDate.getUTCDate() < 10 ? "0" : "") + tsDate.getUTCDate() + "T" +
425          (tsDate.getUTCHours() < 10 ? "0" : "") + tsDate.getUTCHours() + ":" +
426          (tsDate.getUTCMinutes() < 10 ? "0" : "") + tsDate.getUTCMinutes() + ":" +
427          (tsDate.getUTCSeconds() < 10 ? "0" : "") + tsDate.getUTCSeconds() + "Z";
428 }
429
430 function convertTrack(aTargetFormat) {
431   var out = "";
432   switch (aTargetFormat) {
433     case "gpx":
434       out += '<?xml version="1.0" encoding="UTF-8" ?>' + "\n\n";
435       out += '<gpx version="1.0" creator="Lantea" xmlns="http://www.topografix.com/GPX/1/0">' + "\n";
436       if (gTrack.length) {
437         out += '  <trk>' + "\n";
438         out += '    <trkseg>' + "\n";
439         for (var i = 0; i < gTrack.length; i++) {
440           if (gTrack[i].beginSegment && i > 0) {
441             out += '    </trkseg>' + "\n";
442             out += '    <trkseg>' + "\n";
443           }
444           out += '      <trkpt lat="' + gTrack[i].coords.latitude + '" lon="' +
445                                         gTrack[i].coords.longitude + '">' + "\n";
446           if (gTrack[i].coords.altitude) {
447             out += '        <ele>' + gTrack[i].coords.altitude + '</ele>' + "\n";
448           }
449           out += '        <time>' + makeISOString(gTrack[i].time) + '</time>' + "\n";
450           out += '      </trkpt>' + "\n";
451         }
452         out += '    </trkseg>' + "\n";
453         out += '  </trk>' + "\n";
454       }
455       out += '</gpx>' + "\n";
456       break;
457     case "json":
458       out = JSON.stringify(gTrack);
459       break;
460     default:
461       break;
462   }
463   return out;
464 }
465
466 function saveTrack() {
467   if (gTrack.length) {
468     var outDataURI = "data:application/gpx+xml," +
469                      encodeURIComponent(convertTrack("gpx"));
470     window.open(outDataURI, 'GPX Track');
471   }
472 }
473
474 function saveTrackDump() {
475   if (gTrack.length) {
476     var outDataURI = "data:application/json," +
477                      encodeURIComponent(convertTrack("json"));
478     window.open(outDataURI, 'JSON dump');
479   }
480 }
481
482 function uploadTrack() {
483   // Hide all areas in the dialog.
484   var dia = document.getElementById("trackDialogArea");
485   var areas = dia.children;
486   for (var i = 0; i <= areas.length - 1; i++) {
487     areas[i].style.display = "none";
488   }
489   // Reset all the fields in the status area.
490   document.getElementById("uploadStatusCloseButton").disabled = true;
491   document.getElementById("uploadInProgress").style.display = "block";
492   document.getElementById("uploadSuccess").style.display = "none";
493   document.getElementById("uploadFailed").style.display = "none";
494   document.getElementById("uploadError").style.display = "none";
495   document.getElementById("uploadErrorMsg").textContent = "";
496   // Now show the status area.
497   document.getElementById("uploadStatus").style.display = "block";
498
499   // Assemble field to post to the backend.
500   var formData = new FormData();
501   formData.append("jsondata", convertTrack("json"));
502   var desc = document.getElementById("uploadDesc").value;
503   formData.append("comment",
504                   desc.length ? desc : "Track recorded via Lantea Maps");
505   formData.append("devicename",
506                   document.getElementById("uploadDevName").value);
507   formData.append("public",
508                   document.getElementById("uploadPublic").value);
509
510   fetchBackend("save_track", "POST", formData,
511     function(aResult, aStatusCode) {
512       if (aStatusCode >= 400) {
513         reportUploadStatus(false, aResult);
514       }
515       else {
516         reportUploadStatus(true);
517       }
518     }
519   );
520 }
521
522 function reportUploadStatus(aSuccess, aMessage) {
523   document.getElementById("uploadStatusCloseButton").disabled = false;
524   document.getElementById("uploadInProgress").style.display = "none";
525   if (aSuccess) {
526     document.getElementById("uploadSuccess").style.display = "block";
527   }
528   else if (aMessage) {
529     document.getElementById("uploadErrorMsg").textContent = aMessage;
530     document.getElementById("uploadError").style.display = "block";
531   }
532   else {
533     document.getElementById("uploadFailed").style.display = "block";
534   }
535 }
536
537 var gPrefs = {
538   objStore: "prefs",
539
540   get: function(aKey, aCallback) {
541     if (!mainDB)
542       return;
543     var transaction = mainDB.transaction([this.objStore]);
544     var request = transaction.objectStore(this.objStore).get(aKey);
545     request.onsuccess = function(event) {
546       aCallback(request.result, event);
547     };
548     request.onerror = function(event) {
549       // Errors can be handled here.
550       aCallback(undefined, event);
551     };
552   },
553
554   set: function(aKey, aValue, aCallback) {
555     if (!mainDB)
556       return;
557     var success = false;
558     var transaction = mainDB.transaction([this.objStore], "readwrite");
559     var objStore = transaction.objectStore(this.objStore);
560     var request = objStore.put(aValue, aKey);
561     request.onsuccess = function(event) {
562       success = true;
563       if (aCallback)
564         aCallback(success, event);
565     };
566     request.onerror = function(event) {
567       // Errors can be handled here.
568       if (aCallback)
569         aCallback(success, event);
570     };
571   },
572
573   unset: function(aKey, aCallback) {
574     if (!mainDB)
575       return;
576     var success = false;
577     var transaction = mainDB.transaction([this.objStore], "readwrite");
578     var request = transaction.objectStore(this.objStore).delete(aKey);
579     request.onsuccess = function(event) {
580       success = true;
581       if (aCallback)
582         aCallback(success, event);
583     };
584     request.onerror = function(event) {
585       // Errors can be handled here.
586       if (aCallback)
587         aCallback(success, event);
588     }
589   }
590 };
591
592 var gTrackStore = {
593   objStore: "track",
594
595   getList: function(aCallback) {
596     if (!mainDB)
597       return;
598     var transaction = mainDB.transaction([this.objStore]);
599     var objStore = transaction.objectStore(this.objStore);
600     if (objStore.getAll) { // currently Mozilla-specific
601       objStore.getAll().onsuccess = function(event) {
602         aCallback(event.target.result);
603       };
604     }
605     else { // Use cursor (standard method).
606       var tPoints = [];
607       objStore.openCursor().onsuccess = function(event) {
608         var cursor = event.target.result;
609         if (cursor) {
610           tPoints.push(cursor.value);
611           cursor.continue();
612         }
613         else {
614           aCallback(tPoints);
615         }
616       };
617     }
618   },
619
620   getListStepped: function(aCallback) {
621     if (!mainDB)
622       return;
623     var transaction = mainDB.transaction([this.objStore]);
624     var objStore = transaction.objectStore(this.objStore);
625     // Use cursor in reverse direction (so we get the most recent position first)
626     objStore.openCursor(null, "prev").onsuccess = function(event) {
627       var cursor = event.target.result;
628       if (cursor) {
629         aCallback(cursor.value);
630         cursor.continue();
631       }
632       else {
633         aCallback(null);
634       }
635     };
636   },
637
638   push: function(aValue, aCallback) {
639     if (!mainDB)
640       return;
641     var transaction = mainDB.transaction([this.objStore], "readwrite");
642     var objStore = transaction.objectStore(this.objStore);
643     var request = objStore.add(aValue);
644     request.onsuccess = function(event) {
645       if (aCallback)
646         aCallback(request.result, event);
647     };
648     request.onerror = function(event) {
649       // Errors can be handled here.
650       if (aCallback)
651         aCallback(false, event);
652     };
653   },
654
655   clear: function(aCallback) {
656     if (!mainDB)
657       return;
658     var success = false;
659     var transaction = mainDB.transaction([this.objStore], "readwrite");
660     var request = transaction.objectStore(this.objStore).clear();
661     request.onsuccess = function(event) {
662       success = true;
663       if (aCallback)
664         aCallback(success, event);
665     };
666     request.onerror = function(event) {
667       // Errors can be handled here.
668       if (aCallback)
669         aCallback(success, event);
670     }
671   }
672 };
673
674 function fetchBackend(aEndpoint, aMethod, aSendData, aCallback, aCallbackForwards) {
675   var XHR = new XMLHttpRequest();
676   XHR.onreadystatechange = function() {
677     if (XHR.readyState == 4) {
678       // State says we are fully loaded.
679       var result = {};
680       if (XHR.getResponseHeader("Content-Type") == "application/json") {
681         // Got a JSON object, see if we have success.
682         try {
683           result = JSON.parse(XHR.responseText);
684         }
685         catch (e) {
686           console.log(e);
687           result = {"error": e,
688                     "message": XHR.responseText};
689         }
690       }
691       else {
692         result = XHR.responseText;
693       }
694       aCallback(result, XHR.status, aCallbackForwards);
695     }
696   };
697   XHR.open(aMethod, gBackendURL + aEndpoint, true);
698   XHR.withCredentials = "true";
699   //XHR.setRequestHeader("Accept", "application/json");
700   try {
701     XHR.send(aSendData); // Send actual form data.
702   }
703   catch (e) {
704     aCallback(e, 500, aCallbackForwards);
705   }
706 }