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