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