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