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