limit the length of track sections to paint at once on app load; implement a display...
[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 gAction, gActionLabel;
14 var gOSMAPIURL = "http://api.openstreetmap.org/";
15
16 window.onload = function() {
17   gAction = document.getElementById("action");
18   gActionLabel = document.getElementById("actionlabel");
19
20   var mSel = document.getElementById("mapSelector");
21   for (var mapStyle in gMapStyles) {
22     var opt = document.createElement("option");
23     opt.value = mapStyle;
24     opt.text = gMapStyles[mapStyle].name;
25     mSel.add(opt, null);
26   }
27
28   var areas = document.getElementsByClassName("overlayArea");
29   for (var i = 0; i <= areas.length - 1; i++) {
30     areas[i].addEventListener("mouseup", uiEvHandler, false);
31     areas[i].addEventListener("mousemove", uiEvHandler, false);
32     areas[i].addEventListener("mousedown", uiEvHandler, false);
33     areas[i].addEventListener("mouseout", uiEvHandler, false);
34
35     areas[i].addEventListener("touchstart", uiEvHandler, false);
36     areas[i].addEventListener("touchmove", uiEvHandler, false);
37     areas[i].addEventListener("touchend", uiEvHandler, false);
38     areas[i].addEventListener("touchcancel", uiEvHandler, false);
39     areas[i].addEventListener("touchleave", uiEvHandler, false);
40   }
41
42   document.getElementById("body").addEventListener("keydown", uiEvHandler, false);
43
44   if (navigator.platform.length == "") {
45     // For Firefox OS, don't display the "save" button.
46     // Do this by setting the debugHide class for testing in debug mode.
47     document.getElementById("saveTrackButton").classList.add("debugHide");
48   }
49
50   // Without OAuth, the login data is useless
51   //document.getElementById("uploadSettingsArea").classList.remove("debugHide");
52   // As login data is useless for now, always enable upload button
53   document.getElementById("uploadTrackButton").disabled = false;
54
55   if (gDebug) {
56     // Note that GPX upload returns an error 500 on the dev API right now.
57     gOSMAPIURL = "http://api06.dev.openstreetmap.org/";
58   }
59
60   gAction.addEventListener("dbinit-done", initMap, false);
61   gAction.addEventListener("mapinit-done", postInit, false);
62   console.log("starting DB init...");
63   initDB();
64 }
65
66 function postInit(aEvent) {
67   gAction.removeEventListener(aEvent.type, postInit, false);
68   console.log("init done, draw map.");
69   gMapPrefsLoaded = true;
70   gAppInitDone = true;
71   //gMap.resizeAndDraw();  <-- HACK: This triggers bug 1001853, work around with a delay.
72   window.setTimeout(gMap.resizeAndDraw, 100);
73   gActionLabel.textContent = "";
74   gAction.style.display = "none";
75   setTracking(document.getElementById("trackCheckbox"));
76   gPrefs.get(gDebug ? "osm_dev_user" : "osm_user", function(aValue) {
77     if (aValue) {
78       document.getElementById("uploadUser").value = aValue;
79       document.getElementById("uploadTrackButton").disabled = false;
80     }
81   });
82   gPrefs.get(gDebug ? "osm_dev_pwd" : "osm_pwd", function(aValue) {
83     var upwd = document.getElementById("uploadPwd");
84     if (aValue)
85       document.getElementById("uploadPwd").value = aValue;
86   });
87 }
88
89 window.onresize = function() {
90   gMap.resizeAndDraw();
91 }
92
93 function initDB(aEvent) {
94   // Open DB.
95   if (aEvent)
96     gAction.removeEventListener(aEvent.type, initDB, false);
97   var request = window.indexedDB.open("MainDB-lantea", 2);
98   request.onerror = function(event) {
99     // Errors can be handled here. Error codes explain in:
100     // https://developer.mozilla.org/en/IndexedDB/IDBDatabaseException#Constants
101     if (gDebug)
102       console.log("error opening mainDB: " + event.target.errorCode);
103   };
104   request.onsuccess = function(event) {
105     mainDB = request.result;
106     var throwEv = new CustomEvent("dbinit-done");
107     gAction.dispatchEvent(throwEv);
108   };
109   request.onupgradeneeded = function(event) {
110     mainDB = request.result;
111     var ver = mainDB.version || 0; // version is empty string for a new DB
112     if (gDebug)
113       console.log("mainDB has version " + ver + ", upgrade needed.");
114     if (!mainDB.objectStoreNames.contains("prefs")) {
115       // Create a "prefs" objectStore.
116       var prefsStore = mainDB.createObjectStore("prefs");
117     }
118     if (!mainDB.objectStoreNames.contains("track")) {
119       // Create a "track" objectStore.
120       var trackStore = mainDB.createObjectStore("track", {autoIncrement: true});
121     }
122     if (!mainDB.objectStoreNames.contains("tilecache")) {
123       // Create a "tilecache" objectStore.
124       var tilecacheStore = mainDB.createObjectStore("tilecache");
125     }
126     mainDB.onversionchange = function(event) {
127       mainDB.close();
128       mainDB = undefined;
129       initDB();
130     };
131   };
132 }
133
134 function showUI() {
135   if (gUIHideCountdown <= 0) {
136     var areas = document.getElementsByClassName('overlayArea');
137     for (var i = 0; i <= areas.length - 1; i++) {
138       areas[i].classList.remove("hidden");
139     }
140     setTimeout(maybeHideUI, 1000);
141   }
142   gUIHideCountdown = 5;
143 }
144
145 function maybeHideUI() {
146   gUIHideCountdown--;
147   if (document.getElementById("trackArea").style.display == "block") {
148     // If track area is visible, update track data.
149     document.getElementById("trackLength").textContent = calcTrackLength().toFixed(1);
150     document.getElementById("trackDuration").textContent = Math.round(calcTrackDuration()/60);
151   }
152   if (gUIHideCountdown <= 0) {
153     var areas = document.getElementsByClassName('overlayArea');
154     for (var i = 0; i <= areas.length - 1; i++) {
155       areas[i].classList.add("hidden");
156     }
157   }
158   else {
159     setTimeout(maybeHideUI, 1000);
160   }
161 }
162
163 function toggleTrackArea() {
164   var fs = document.getElementById("trackArea");
165   if (fs.style.display != "block") {
166     fs.style.display = "block";
167     showUI();
168   }
169   else {
170     fs.style.display = "none";
171   }
172 }
173
174 function toggleSettings() {
175   var fs = document.getElementById("settingsArea");
176   if (fs.style.display != "block") {
177     fs.style.display = "block";
178     showUI();
179   }
180   else {
181     fs.style.display = "none";
182   }
183 }
184
185 function toggleFullscreen() {
186   if ((document.fullScreenElement && document.fullScreenElement !== null) ||
187       (document.mozFullScreenElement && document.mozFullScreenElement !== null) ||
188       (document.webkitFullScreenElement && document.webkitFullScreenElement !== null)) {
189     if (document.cancelFullScreen) {
190       document.cancelFullScreen();
191     } else if (document.mozCancelFullScreen) {
192       document.mozCancelFullScreen();
193     } else if (document.webkitCancelFullScreen) {
194       document.webkitCancelFullScreen();
195     }
196   }
197   else {
198     var elem = document.getElementById("body");
199     if (elem.requestFullScreen) {
200       elem.requestFullScreen();
201     } else if (elem.mozRequestFullScreen) {
202       elem.mozRequestFullScreen();
203     } else if (elem.webkitRequestFullScreen) {
204       elem.webkitRequestFullScreen();
205     }
206   }
207 }
208
209 function showUploadDialog() {
210   var dia = document.getElementById("dialogArea");
211   var areas = dia.children;
212   for (var i = 0; i <= areas.length - 1; i++) {
213     areas[i].style.display = "none";
214   }
215   document.getElementById("uploadDialog").style.display = "block";
216   document.getElementById("uploadTrackButton").disabled = true;
217   dia.classList.remove("hidden");
218 }
219
220 function showGLWarningDialog() {
221   var dia = document.getElementById("dialogArea");
222   var areas = dia.children;
223   for (var i = 0; i <= areas.length - 1; i++) {
224     areas[i].style.display = "none";
225   }
226   document.getElementById("noGLwarning").style.display = "block";
227   dia.classList.remove("hidden");
228 }
229
230 function cancelDialog() {
231   document.getElementById("dialogArea").classList.add("hidden");
232   document.getElementById("uploadTrackButton").disabled = false;
233 }
234
235 var uiEvHandler = {
236   handleEvent: function(aEvent) {
237     var touchEvent = aEvent.type.indexOf('touch') != -1;
238
239     switch (aEvent.type) {
240       case "mousedown":
241       case "touchstart":
242       case "mousemove":
243       case "touchmove":
244       case "mouseup":
245       case "touchend":
246       case "keydown":
247         showUI();
248         break;
249     }
250   }
251 };
252
253 function setUploadField(aField) {
254   switch (aField.id) {
255     case "uploadUser":
256       gPrefs.set(gDebug ? "osm_dev_user" : "osm_user", aField.value);
257       document.getElementById("uploadTrackButton").disabled = !aField.value.length;
258       break;
259     case "uploadPwd":
260       gPrefs.set(gDebug ? "osm_dev_pwd" : "osm_pwd", aField.value);
261       break;
262   }
263 }
264
265 function makeISOString(aTimestamp) {
266   // ISO time format is YYYY-MM-DDTHH:mm:ssZ
267   var tsDate = new Date(aTimestamp);
268   // Note that .getUTCMonth() returns a number between 0 and 11 (0 for January)!
269   return tsDate.getUTCFullYear() + "-" +
270          (tsDate.getUTCMonth() < 9 ? "0" : "") + (tsDate.getUTCMonth() + 1 ) + "-" +
271          (tsDate.getUTCDate() < 10 ? "0" : "") + tsDate.getUTCDate() + "T" +
272          (tsDate.getUTCHours() < 10 ? "0" : "") + tsDate.getUTCHours() + ":" +
273          (tsDate.getUTCMinutes() < 10 ? "0" : "") + tsDate.getUTCMinutes() + ":" +
274          (tsDate.getUTCSeconds() < 10 ? "0" : "") + tsDate.getUTCSeconds() + "Z";
275 }
276
277 function convertTrack(aTargetFormat) {
278   var out = "";
279   switch (aTargetFormat) {
280     case "gpx":
281       out += '<?xml version="1.0" encoding="UTF-8" ?>' + "\n\n";
282       out += '<gpx version="1.0" creator="Lantea" xmlns="http://www.topografix.com/GPX/1/0">' + "\n";
283       if (gTrack.length) {
284         out += '  <trk>' + "\n";
285         out += '    <trkseg>' + "\n";
286         for (var i = 0; i < gTrack.length; i++) {
287           if (gTrack[i].beginSegment && i > 0) {
288             out += '    </trkseg>' + "\n";
289             out += '    <trkseg>' + "\n";
290           }
291           out += '      <trkpt lat="' + gTrack[i].coords.latitude + '" lon="' +
292                                         gTrack[i].coords.longitude + '">' + "\n";
293           if (gTrack[i].coords.altitude) {
294             out += '        <ele>' + gTrack[i].coords.altitude + '</ele>' + "\n";
295           }
296           out += '        <time>' + makeISOString(gTrack[i].time) + '</time>' + "\n";
297           out += '      </trkpt>' + "\n";
298         }
299         out += '    </trkseg>' + "\n";
300         out += '  </trk>' + "\n";
301       }
302       out += '</gpx>' + "\n";
303       break;
304     case "json":
305       out = JSON.stringify(gTrack);
306       break;
307     default:
308       break;
309   }
310   return out;
311 }
312
313 function saveTrack() {
314   if (gTrack.length) {
315     var outDataURI = "data:application/gpx+xml," +
316                      encodeURIComponent(convertTrack("gpx"));
317     window.open(outDataURI, 'GPX Track');
318   }
319 }
320
321 function saveTrackDump() {
322   if (gTrack.length) {
323     var outDataURI = "data:application/json," +
324                      encodeURIComponent(convertTrack("json"));
325     window.open(outDataURI, 'JSON dump');
326   }
327 }
328
329 function uploadTrack() {
330   // Hide all areas in the dialog.
331   var dia = document.getElementById("dialogArea");
332   var areas = dia.children;
333   for (var i = 0; i <= areas.length - 1; i++) {
334     areas[i].style.display = "none";
335   }
336   // Reset all the fields in the status area.
337   document.getElementById("uploadStatusCloseButton").disabled = true;
338   document.getElementById("uploadInProgress").style.display = "block";
339   document.getElementById("uploadSuccess").style.display = "none";
340   document.getElementById("uploadFailed").style.display = "none";
341   document.getElementById("uploadError").style.display = "none";
342   document.getElementById("uploadErrorMsg").textContent = "";
343   // Now show the status area.
344   document.getElementById("uploadStatus").style.display = "block";
345
346   // See http://wiki.openstreetmap.org/wiki/Api06#Uploading_traces
347   var trackBlob = new Blob([convertTrack("gpx")],
348                            { "type" : "application/gpx+xml" });
349   var formData = new FormData();
350   formData.append("file", trackBlob);
351   var desc = document.getElementById("uploadDesc").value;
352   formData.append("description",
353                   desc.length ? desc : "Track recorded via Lantea Maps");
354   //formData.append("tags", "");
355   formData.append("visibility",
356                   document.getElementById("uploadVisibility").value);
357   // Do an empty POST request first, so that we don't send everything,
358   // then ask for credentials, and then send again.
359   var hXHR = new XMLHttpRequest();
360   hXHR.onreadystatechange = function() {
361     if (hXHR.readyState == 4 && (hXHR.status == 200 || hXHR.status == 400)) {
362       // 400 is Bad Request, but that's expected as this was empty.
363       // So far so good, init actual upload.
364       var XHR = new XMLHttpRequest();
365       XHR.onreadystatechange = function() {
366         if (XHR.readyState == 4 && XHR.status == 200) {
367           // Everthing looks fine.
368           reportUploadStatus(true);
369         } else if (XHR.readyState == 4 && XHR.status != 200) {
370           // Fetched the wrong page or network error...
371           reportUploadStatus(false);
372         }
373       };
374       XHR.open("POST", gOSMAPIURL + "api/0.6/gpx/create", true);
375       // Cross-Origin XHR doesn't allow username/password (HTTP Auth).
376       // So, we'll ask the user for entering credentials with rather ugly UI.
377       XHR.withCredentials = true;
378       try {
379         XHR.send(formData); // Send actual form data.
380       }
381       catch (e) {
382         reportUploadStatus(false, e);
383       }
384     } else if (hXHR.readyState == 4 && hXHR.status != 200) {
385       // Fetched the wrong page or network error...
386       reportUploadStatus(false);
387     }
388   };
389   hXHR.open("POST", gOSMAPIURL + "api/0.6/gpx/create", true);
390   // Cross-Origin XHR doesn't allow username/password (HTTP Auth).
391   // So, we'll ask the user for entering credentials with rather ugly UI.
392   hXHR.withCredentials = true;
393   try {
394     hXHR.send(); // Empty request, see above.
395   }
396   catch (e) {
397     reportUploadStatus(false, e);
398   }
399 }
400
401 function reportUploadStatus(aSuccess, aMessage) {
402   document.getElementById("uploadStatusCloseButton").disabled = false;
403   document.getElementById("uploadInProgress").style.display = "none";
404   if (aSuccess) {
405     document.getElementById("uploadSuccess").style.display = "block";
406   }
407   else if (aMessage) {
408     document.getElementById("uploadErrorMsg").textContent = aMessage;
409     document.getElementById("uploadError").style.display = "block";
410   }
411   else {
412     document.getElementById("uploadFailed").style.display = "block";
413   }
414 }
415
416 var gPrefs = {
417   objStore: "prefs",
418
419   get: function(aKey, aCallback) {
420     if (!mainDB)
421       return;
422     var transaction = mainDB.transaction([this.objStore]);
423     var request = transaction.objectStore(this.objStore).get(aKey);
424     request.onsuccess = function(event) {
425       aCallback(request.result, event);
426     };
427     request.onerror = function(event) {
428       // Errors can be handled here.
429       aCallback(undefined, event);
430     };
431   },
432
433   set: function(aKey, aValue, aCallback) {
434     if (!mainDB)
435       return;
436     var success = false;
437     var transaction = mainDB.transaction([this.objStore], "readwrite");
438     var objStore = transaction.objectStore(this.objStore);
439     var request = objStore.put(aValue, aKey);
440     request.onsuccess = function(event) {
441       success = true;
442       if (aCallback)
443         aCallback(success, event);
444     };
445     request.onerror = function(event) {
446       // Errors can be handled here.
447       if (aCallback)
448         aCallback(success, event);
449     };
450   },
451
452   unset: function(aKey, aCallback) {
453     if (!mainDB)
454       return;
455     var success = false;
456     var transaction = mainDB.transaction([this.objStore], "readwrite");
457     var request = transaction.objectStore(this.objStore).delete(aKey);
458     request.onsuccess = function(event) {
459       success = true;
460       if (aCallback)
461         aCallback(success, event);
462     };
463     request.onerror = function(event) {
464       // Errors can be handled here.
465       if (aCallback)
466         aCallback(success, event);
467     }
468   }
469 };
470
471 var gTrackStore = {
472   objStore: "track",
473
474   getList: function(aCallback) {
475     if (!mainDB)
476       return;
477     var transaction = mainDB.transaction([this.objStore]);
478     var objStore = transaction.objectStore(this.objStore);
479     if (objStore.getAll) { // currently Mozilla-specific
480       objStore.getAll().onsuccess = function(event) {
481         aCallback(event.target.result);
482       };
483     }
484     else { // Use cursor (standard method).
485       var tPoints = [];
486       objStore.openCursor().onsuccess = function(event) {
487         var cursor = event.target.result;
488         if (cursor) {
489           tPoints.push(cursor.value);
490           cursor.continue();
491         }
492         else {
493           aCallback(tPoints);
494         }
495       };
496     }
497   },
498
499   getListStepped: function(aCallback) {
500     if (!mainDB)
501       return;
502     var transaction = mainDB.transaction([this.objStore]);
503     var objStore = transaction.objectStore(this.objStore);
504     // Use cursor in reverse direction (so we get the most recent position first)
505     objStore.openCursor(null, "prev").onsuccess = function(event) {
506       var cursor = event.target.result;
507       if (cursor) {
508         aCallback(cursor.value);
509         cursor.continue();
510       }
511       else {
512         aCallback(null);
513       }
514     };
515   },
516
517   push: function(aValue, aCallback) {
518     if (!mainDB)
519       return;
520     var transaction = mainDB.transaction([this.objStore], "readwrite");
521     var objStore = transaction.objectStore(this.objStore);
522     var request = objStore.add(aValue);
523     request.onsuccess = function(event) {
524       if (aCallback)
525         aCallback(request.result, event);
526     };
527     request.onerror = function(event) {
528       // Errors can be handled here.
529       if (aCallback)
530         aCallback(false, event);
531     };
532   },
533
534   clear: function(aCallback) {
535     if (!mainDB)
536       return;
537     var success = false;
538     var transaction = mainDB.transaction([this.objStore], "readwrite");
539     var request = transaction.objectStore(this.objStore).clear();
540     request.onsuccess = function(event) {
541       success = true;
542       if (aCallback)
543         aCallback(success, event);
544     };
545     request.onerror = function(event) {
546       // Errors can be handled here.
547       if (aCallback)
548         aCallback(success, event);
549     }
550   }
551 };