a25a1f5617f6d6163fcf25d9f920c8ff6f4496ab
[lantea.git] / js / map.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 var gMapCanvas, gMapContext, gGLMapCanvas, gTrackCanvas, gTrackContext, gGeolocation;
6 var gDebug = false;
7
8 var gTileSize = 256;
9 var gMaxZoom = 18; // The minimum is 0.
10
11 var gMinTrackAccuracy = 1000; // meters
12 var gTrackWidth = 2; // pixels
13 var gTrackColor = "#FF0000";
14 var gCurLocSize = 6; // pixels
15 var gCurLocColor = "#A00000";
16
17 var gMapStyles = {
18   // OSM tile usage policy: http://wiki.openstreetmap.org/wiki/Tile_usage_policy
19   // Find some more OSM ones at http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
20   osm_mapnik:
21     {name: "OpenStreetMap (Mapnik)",
22      url: "http://tile.openstreetmap.org/{z}/{x}/{y}.png",
23      copyright: 'Map data and imagery &copy; <a href="http://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="http://www.openstreetmap.org/copyright">ODbL/CC-BY-SA</a>'},
24   osm_cyclemap:
25     {name: "Cycle Map (OSM)",
26      url: "http://[a-c].tile.opencyclemap.org/cycle/{z}/{x}/{y}.png",
27      copyright: 'Map data and imagery &copy; <a href="http://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="http://www.openstreetmap.org/copyright">ODbL/CC-BY-SA</a>'},
28   osm_transmap:
29     {name: "Transport Map (OSM)",
30      url: "http://[a-c].tile2.opencyclemap.org/transport/{z}/{x}/{y}.png",
31      copyright: 'Map data and imagery &copy; <a href="http://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="http://www.openstreetmap.org/copyright">ODbL/CC-BY-SA</a>'},
32   mapquest_open:
33     {name: "MapQuest OSM",
34      url: "http://otile[1-4].mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png",
35      copyright: 'Map data &copy; <a href="http://www.openstreetmap.org/">OpenStreetMap</a> and contributors (<a href="http://www.openstreetmap.org/copyright">ODbL/CC-BY-SA</a>), tiles Courtesy of <a href="http://www.mapquest.com/">MapQuest</a>.'},
36   mapquest_aerial:
37     {name: "MapQuest Open Aerial",
38      url: "http://otile[1-4].mqcdn.com/tiles/1.0.0/sat/{z}/{x}/{y}.jpg",
39      copyright: 'Tiles Courtesy of <a href="http://www.mapquest.com/">MapQuest</a>, portions Courtesy NASA/JPL-Caltech and U.S. Depart. of Agriculture, Farm Service Agency.'},
40   opengeoserver_arial:
41     {name: "OpenGeoServer Aerial",
42      url: "http://services.opengeoserver.org/tiles/1.0.0/globe.aerial_EPSG3857/{z}/{x}/{y}.png?origin=nw",
43      copyright: 'Tiles by <a href="http://www.opengeoserver.org/">OpenGeoServer.org</a>, <a href="https://creativecommons.org/licenses/by/3.0/at/">CC-BY 3.0 AT</a>.'},
44   google_map:
45     {name: "Google Maps",
46      url: " http://mt1.google.com/vt/x={x}&y={y}&z={z}",
47      copyright: 'Map data and imagery &copy; <a href="http://maps.google.com/">Google</a>'},
48 };
49 var gActiveMap = "osm_mapnik";
50
51 var gPos = {x: 35630000.0, // Current position in the map in pixels at the maximum zoom level (18)
52             y: 23670000.0, // The range is 0-67108864 (2^gMaxZoom * gTileSize)
53             z: 5}; // This could be fractional if supported being between zoom levels.
54
55 var gLastMouseX = 0;
56 var gLastMouseY = 0;
57 var gZoomFactor;
58
59 var gLoadingTile;
60
61 var gMapPrefsLoaded = false;
62
63 var gDragging = false;
64 var gDragTouchID, gPinchStartWidth;
65
66 var gGeoWatchID;
67 var gTrack = [];
68 var gLastTrackPoint, gLastDrawnPoint;
69 var gCenterPosition = true;
70
71 var gCurPosMapCache;
72
73 function initMap() {
74   gGeolocation = navigator.geolocation;
75   // Set up canvas contexts. TODO: Remove 2D map once GL support works.
76   gMapCanvas = document.getElementById("map");
77   gMapContext = gMapCanvas.getContext("2d");
78   gGLMapCanvas = document.getElementById("glmap");
79   try {
80     // Try to grab the standard context. If it fails, fallback to experimental.
81     // We also try to tell it we do not need a depth buffer.
82     gMap.gl = gGLMapCanvas.getContext("webgl", {depth: false}) ||
83               gGLMapCanvas.getContext("experimental-webgl", {depth: false});
84   }
85   catch(e) {}
86   // If we don't have a GL context, give up now
87   if (!gMap.gl) {
88     showGLWarningDialog();
89     gMap.gl = null;
90   }
91   gTrackCanvas = document.getElementById("track");
92   gTrackContext = gTrackCanvas.getContext("2d");
93   if (!gActiveMap)
94     gActiveMap = "osm_mapnik";
95
96   //gDebug = true;
97   if (gDebug) {
98     gGeolocation = geofake;
99     var hiddenList = document.getElementsByClassName("debugHide");
100     // last to first - list of elements with that class is changing!
101     for (var i = hiddenList.length - 1; i >= 0; i--) {
102       hiddenList[i].classList.remove("debugHide");
103     }
104   }
105
106   gAction.addEventListener("prefload-done", gMap.initGL, false);
107
108   console.log("map vars set, loading prefs...");
109   loadPrefs();
110 }
111
112 function loadPrefs(aEvent) {
113   if (aEvent && aEvent.type == "prefs-step") {
114     console.log("wait: " + gWaitCounter);
115     if (gWaitCounter == 0) {
116       gAction.removeEventListener(aEvent.type, loadPrefs, false);
117       gMapPrefsLoaded = true;
118       console.log("prefs loaded.");
119
120       gTrackCanvas.addEventListener("mouseup", mapEvHandler, false);
121       gTrackCanvas.addEventListener("mousemove", mapEvHandler, false);
122       gTrackCanvas.addEventListener("mousedown", mapEvHandler, false);
123       gTrackCanvas.addEventListener("mouseout", mapEvHandler, false);
124
125       gTrackCanvas.addEventListener("touchstart", mapEvHandler, false);
126       gTrackCanvas.addEventListener("touchmove", mapEvHandler, false);
127       gTrackCanvas.addEventListener("touchend", mapEvHandler, false);
128       gTrackCanvas.addEventListener("touchcancel", mapEvHandler, false);
129       gTrackCanvas.addEventListener("touchleave", mapEvHandler, false);
130
131       gTrackCanvas.addEventListener("wheel", mapEvHandler, false);
132
133       document.getElementById("body").addEventListener("keydown", mapEvHandler, false);
134
135       document.getElementById("copyright").innerHTML =
136           gMapStyles[gActiveMap].copyright;
137
138       gLoadingTile = new Image();
139       gLoadingTile.src = "style/loading.png";
140       gLoadingTile.onload = function() {
141         var throwEv = new CustomEvent("prefload-done");
142         gAction.dispatchEvent(throwEv);
143       };
144     }
145   }
146   else {
147     if (aEvent)
148       gAction.removeEventListener(aEvent.type, loadPrefs, false);
149     gAction.addEventListener("prefs-step", loadPrefs, false);
150     gWaitCounter++;
151     gPrefs.get("position", function(aValue) {
152       if (aValue) {
153         gPos = aValue;
154       }
155       gWaitCounter--;
156       var throwEv = new CustomEvent("prefs-step");
157       gAction.dispatchEvent(throwEv);
158     });
159     gWaitCounter++;
160     gPrefs.get("center_map", function(aValue) {
161       if (aValue === undefined)
162         document.getElementById("centerCheckbox").checked = true;
163       else
164         document.getElementById("centerCheckbox").checked = aValue;
165       setCentering(document.getElementById("centerCheckbox"));
166       gWaitCounter--;
167       var throwEv = new CustomEvent("prefs-step");
168       gAction.dispatchEvent(throwEv);
169     });
170     gWaitCounter++;
171     gPrefs.get("tracking_enabled", function(aValue) {
172       if (aValue === undefined)
173         document.getElementById("trackCheckbox").checked = true;
174       else
175         document.getElementById("trackCheckbox").checked = aValue;
176       gWaitCounter--;
177       var throwEv = new CustomEvent("prefs-step");
178       gAction.dispatchEvent(throwEv);
179     });
180     gWaitCounter++;
181     var trackLoadStarted = false;
182     var redrawBase = 100;
183     gTrackStore.getListStepped(function(aTPoint) {
184       if (aTPoint) {
185         // Add in front and return new length.
186         var tracklen = gTrack.unshift(aTPoint);
187         // Redraw track periodically, larger distance the longer it gets.
188         // Initial paint will do initial track drawing.
189         if (tracklen % redrawBase == 0) {
190           drawTrack();
191           redrawBase = tracklen;
192         }
193       }
194       else {
195         // Last point received.
196         drawTrack();
197       }
198       if (!trackLoadStarted) {
199         // We have the most recent point, if present, rest will load async.
200         trackLoadStarted = true;
201         gWaitCounter--;
202         var throwEv = new CustomEvent("prefs-step");
203         gAction.dispatchEvent(throwEv);
204       }
205     });
206   }
207 }
208
209 var gMap = {
210   gl: null,
211   glShaderProgram: null,
212   glVertexPositionAttr: null,
213   glTextureCoordAttr: null,
214   glResolutionAttr: null,
215   glMapTexture: null,
216
217   getVertShaderSource: function() {
218     return 'attribute vec2 aVertexPosition;\n' +
219     'attribute vec2 aTextureCoord;\n\n' +
220     'uniform vec2 uResolution;\n\n' +
221     'varying highp vec2 vTextureCoord;\n\n' +
222     'void main(void) {\n' +
223     // convert the rectangle from pixels to -1.0 to +1.0 (clipspace) 0.0 to 1.0
224     '  vec2 clipSpace = aVertexPosition * 2.0 / uResolution - 1.0;\n' +
225     '  gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);\n' +
226     '  vTextureCoord = aTextureCoord;\n' +
227     '}'; },
228   getFragShaderSource:function() {
229     return 'varying highp vec2 vTextureCoord;\n\n' +
230     'uniform sampler2D uImage;\n\n' +
231     'void main(void) {\n' +
232     '  gl_FragColor = texture2D(uImage, vTextureCoord);\n' +
233     '}'; },
234
235   initGL: function() {
236     // When called from the event listener, the "this" reference doesn't work, so use the object name.
237     if (gMap.gl) {
238       gMap.gl.viewport(0, 0, gMap.gl.drawingBufferWidth, gMap.gl.drawingBufferHeight);
239       gMap.gl.clearColor(0.0, 0.0, 0.0, 0.5);                          // Set clear color to black, fully opaque.
240       gMap.gl.clear(gMap.gl.COLOR_BUFFER_BIT|gMap.gl.DEPTH_BUFFER_BIT);  // Clear the color.
241
242       // Create and initialize the shaders.
243       var vertShader = gMap.gl.createShader(gMap.gl.VERTEX_SHADER);
244       var fragShader = gMap.gl.createShader(gMap.gl.FRAGMENT_SHADER);
245       gMap.gl.shaderSource(vertShader, gMap.getVertShaderSource());
246       // Compile the shader program.
247       gMap.gl.compileShader(vertShader);
248       // See if it compiled successfully.
249       if (!gMap.gl.getShaderParameter(vertShader, gMap.gl.COMPILE_STATUS)) {
250         console.log("An error occurred compiling the vertex shader: " + gMap.gl.getShaderInfoLog(vertShader));
251         return null;
252       }
253       gMap.gl.shaderSource(fragShader, gMap.getFragShaderSource());
254       // Compile the shader program.
255       gMap.gl.compileShader(fragShader);
256       // See if it compiled successfully.
257       if (!gMap.gl.getShaderParameter(fragShader, gMap.gl.COMPILE_STATUS)) {
258         console.log("An error occurred compiling the fragment shader: " + gMap.gl.getShaderInfoLog(fragShader));
259         return null;
260       }
261
262       gMap.glShaderProgram = gMap.gl.createProgram();
263       gMap.gl.attachShader(gMap.glShaderProgram, vertShader);
264       gMap.gl.attachShader(gMap.glShaderProgram, fragShader);
265       gMap.gl.linkProgram(gMap.glShaderProgram);
266       // If creating the shader program failed, alert
267       if (!gMap.gl.getProgramParameter(gMap.glShaderProgram, gMap.gl.LINK_STATUS)) {
268         alert("Unable to initialize the shader program.");
269       }
270       gMap.gl.useProgram(gMap.glShaderProgram);
271       // Get locations of the attributes.
272       gMap.glVertexPositionAttr = gMap.gl.getAttribLocation(gMap.glShaderProgram, "aVertexPosition");
273       gMap.glTextureCoordAttr = gMap.gl.getAttribLocation(gMap.glShaderProgram, "aTextureCoord");
274       gMap.glResolutionAttr = gMap.gl.getUniformLocation(gMap.glShaderProgram, "uResolution");
275
276       var tileVerticesBuffer = gMap.gl.createBuffer();
277       gMap.gl.bindBuffer(gMap.gl.ARRAY_BUFFER, tileVerticesBuffer);
278       // The vertices are the coordinates of the corner points of the square.
279       var vertices = [
280         0.0,  0.0,
281         1.0,  0.0,
282         0.0,  1.0,
283         0.0,  1.0,
284         1.0,  0.0,
285         1.0,  1.0,
286       ];
287       gMap.gl.bufferData(gMap.gl.ARRAY_BUFFER, new Float32Array(vertices), gMap.gl.STATIC_DRAW);
288       gMap.gl.enableVertexAttribArray(gMap.glTextureCoordAttr);
289       gMap.gl.vertexAttribPointer(gMap.glTextureCoordAttr, 2, gMap.gl.FLOAT, false, 0, 0);
290
291       // Map Texture
292       gMap.glMapTexture = gMap.gl.createTexture();
293       gMap.gl.activeTexture(gMap.gl.TEXTURE0);
294       gMap.gl.bindTexture(gMap.gl.TEXTURE_2D, gMap.glMapTexture);
295       gMap.gl.uniform1i(gMap.gl.getUniformLocation(gMap.glShaderProgram, "uImage"), 0);
296       // Set params for how the texture minifies and magnifies (wrap params are not needed as we're power-of-two).
297       gMap.gl.texParameteri(gMap.gl.TEXTURE_2D, gMap.gl.TEXTURE_MIN_FILTER, gMap.gl.NEAREST);
298       gMap.gl.texParameteri(gMap.gl.TEXTURE_2D, gMap.gl.TEXTURE_MAG_FILTER, gMap.gl.NEAREST);
299       // Upload the image into the texture.
300       gMap.gl.texImage2D(gMap.gl.TEXTURE_2D, 0, gMap.gl.RGBA, gMap.gl.RGBA, gMap.gl.UNSIGNED_BYTE, gLoadingTile);
301
302       gMap.gl.uniform2f(gMap.glResolutionAttr, gGLMapCanvas.width, gGLMapCanvas.height);
303
304       // Create a buffer for the position of the rectangle corners.
305       var mapVerticesTextureCoordBuffer = gMap.gl.createBuffer();
306       gMap.gl.bindBuffer(gMap.gl.ARRAY_BUFFER, mapVerticesTextureCoordBuffer);
307       gMap.gl.enableVertexAttribArray(gMap.glVertexPositionAttr);
308       gMap.gl.vertexAttribPointer(gMap.glVertexPositionAttr, 2, gMap.gl.FLOAT, false, 0, 0);
309     }
310
311     var throwEv = new CustomEvent("mapinit-done");
312     gAction.dispatchEvent(throwEv);
313   },
314
315   drawGL: function() {
316     if (!gMap.gl) { return; }
317
318     var x_start = 0;
319     var i_width = 256;
320     var y_start = 10;
321     var i_height = 256;
322     var textureCoordinates = [
323       x_start, y_start,
324       x_start + i_width, y_start,
325       x_start, y_start + i_height,
326       x_start, y_start + i_height,
327       x_start + i_width, y_start,
328       x_start + i_width, y_start + i_height,
329     ];
330     gMap.gl.bufferData(gMap.gl.ARRAY_BUFFER, new Float32Array(textureCoordinates), gMap.gl.STATIC_DRAW);
331
332     // There are 6 indices in textureCoordinates.
333     gMap.gl.drawArrays(gMap.gl.TRIANGLES, 0, 6);
334
335     var x_start = 400;
336     var i_width = 256;
337     var y_start = 10;
338     var i_height = 256;
339     var textureCoordinates = [
340       x_start, y_start,
341       x_start + i_width, y_start,
342       x_start, y_start + i_height,
343       x_start, y_start + i_height,
344       x_start + i_width, y_start,
345       x_start + i_width, y_start + i_height,
346     ];
347     gMap.gl.bufferData(gMap.gl.ARRAY_BUFFER, new Float32Array(textureCoordinates), gMap.gl.STATIC_DRAW);
348     // gMap.gl.bindTexture(gMap.gl.TEXTURE_2D, gMap.glMapTexture);
349
350     // There are 6 indices in textureCoordinates.
351     gMap.gl.drawArrays(gMap.gl.TRIANGLES, 0, 6);
352
353   },
354
355   resizeAndDrawGL: function() {
356     if (!gMap.gl) { return; }
357
358     gMap.gl.viewport(0, 0, gMap.gl.drawingBufferWidth, gMap.gl.drawingBufferHeight);
359     gMap.gl.clear(gMap.gl.COLOR_BUFFER_BIT);  // Clear the color.
360     gMap.gl.uniform2f(gMap.glResolutionAttr, gGLMapCanvas.width, gGLMapCanvas.height);
361     gMap.drawGL();
362   },
363 }
364
365 function resizeAndDraw() {
366   var viewportWidth = Math.min(window.innerWidth, window.outerWidth);
367   var viewportHeight = Math.min(window.innerHeight, window.outerHeight);
368   if (gMapCanvas && gGLMapCanvas && gTrackCanvas) {
369     gMapCanvas.width = viewportWidth;
370     gMapCanvas.height = viewportHeight;
371     gGLMapCanvas.width = viewportWidth;
372     gGLMapCanvas.height = viewportHeight;
373     gTrackCanvas.width = viewportWidth;
374     gTrackCanvas.height = viewportHeight;
375     drawMap();
376     gMap.resizeAndDrawGL();
377     showUI();
378   }
379 }
380
381 // Using scale(x, y) together with drawing old data on scaled canvas would be an improvement for zooming.
382 // See https://developer.mozilla.org/en-US/docs/Canvas_tutorial/Transformations#Scaling
383
384 function zoomIn() {
385   if (gPos.z < gMaxZoom) {
386     gPos.z++;
387     drawMap();
388   }
389 }
390
391 function zoomOut() {
392   if (gPos.z > 0) {
393     gPos.z--;
394     drawMap();
395   }
396 }
397
398 function zoomTo(aTargetLevel) {
399   aTargetLevel = parseInt(aTargetLevel);
400   if (aTargetLevel >= 0 && aTargetLevel <= gMaxZoom) {
401     gPos.z = aTargetLevel;
402     drawMap();
403   }
404 }
405
406 function gps2xy(aLatitude, aLongitude) {
407   var maxZoomFactor = Math.pow(2, gMaxZoom) * gTileSize;
408   var convLat = aLatitude * Math.PI / 180;
409   var rawY = (1 - Math.log(Math.tan(convLat) +
410                            1 / Math.cos(convLat)) / Math.PI) / 2 * maxZoomFactor;
411   var rawX = (aLongitude + 180) / 360 * maxZoomFactor;
412   return {x: Math.round(rawX),
413           y: Math.round(rawY)};
414 }
415
416 function xy2gps(aX, aY) {
417   var maxZoomFactor = Math.pow(2, gMaxZoom) * gTileSize;
418   var n = Math.PI - 2 * Math.PI * aY / maxZoomFactor;
419   return {latitude: 180 / Math.PI *
420                     Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))),
421           longitude: aX / maxZoomFactor * 360 - 180};
422 }
423
424 function setMapStyle() {
425   var mapSel = document.getElementById("mapSelector");
426   if (mapSel.selectedIndex >= 0 && gActiveMap != mapSel.value) {
427     gActiveMap = mapSel.value;
428     document.getElementById("copyright").innerHTML =
429         gMapStyles[gActiveMap].copyright;
430     showUI();
431     drawMap();
432   }
433 }
434
435 // A sane mod function that works for negative numbers.
436 // Returns a % b.
437 function mod(a, b) {
438   return ((a % b) + b) % b;
439 }
440
441 function normalizeCoords(aCoords) {
442   var zoomFactor = Math.pow(2, aCoords.z);
443   return {x: mod(aCoords.x, zoomFactor),
444           y: mod(aCoords.y, zoomFactor),
445           z: aCoords.z};
446 }
447
448 // Returns true if the tile is outside the current view.
449 function isOutsideWindow(t) {
450   var pos = decodeIndex(t);
451
452   var zoomFactor = Math.pow(2, gMaxZoom - pos.z);
453   var wid = gMapCanvas.width * zoomFactor;
454   var ht = gMapCanvas.height * zoomFactor;
455
456   pos.x *= zoomFactor;
457   pos.y *= zoomFactor;
458
459   var sz = gTileSize * zoomFactor;
460   if (pos.x > gPos.x + wid / 2 || pos.y > gPos.y + ht / 2 ||
461       pos.x + sz < gPos.x - wid / 2 || pos.y - sz < gPos.y - ht / 2)
462     return true;
463   return false;
464 }
465
466 function encodeIndex(x, y, z) {
467   var norm = normalizeCoords({x: x, y: y, z: z});
468   return norm.x + "," + norm.y + "," + norm.z;
469 }
470
471 function decodeIndex(encodedIdx) {
472   var ind = encodedIdx.split(",", 3);
473   return {x: ind[0], y: ind[1], z: ind[2]};
474 }
475
476 function drawMap(aPixels, aOverdraw) {
477   // aPixels is an object with left/right/top/bottom members telling how many
478   //   pixels on the borders should actually be drawn.
479   // aOverdraw is a bool that tells if we should draw placeholders or draw
480   //   straight over the existing content.
481   if (!aPixels)
482     aPixels = {left: gMapCanvas.width, right: gMapCanvas.width,
483                top: gMapCanvas.height, bottom: gMapCanvas.height};
484   if (!aOverdraw)
485     aOverdraw = false;
486
487   document.getElementById("zoomLevel").textContent = gPos.z;
488   gZoomFactor = Math.pow(2, gMaxZoom - gPos.z);
489   var wid = gMapCanvas.width * gZoomFactor; // Width in level 18 pixels.
490   var ht = gMapCanvas.height * gZoomFactor; // Height in level 18 pixels.
491   var size = gTileSize * gZoomFactor; // Tile size in level 18 pixels.
492
493   var xMin = gPos.x - wid / 2; // Corners of the window in level 18 pixels.
494   var yMin = gPos.y - ht / 2;
495   var xMax = gPos.x + wid / 2;
496   var yMax = gPos.y + ht / 2;
497
498   if (gMapPrefsLoaded && mainDB)
499     gPrefs.set("position", gPos);
500
501   var tiles = {left: Math.ceil((xMin + aPixels.left * gZoomFactor) / size) -
502                                (aPixels.left ? 0 : 1),
503                right: Math.floor((xMax - aPixels.right * gZoomFactor) / size) -
504                                  (aPixels.right ? 1 : 0),
505                top: Math.ceil((yMin + aPixels.top * gZoomFactor) / size) -
506                               (aPixels.top ? 0 : 1),
507                bottom: Math.floor((yMax - aPixels.bottom * gZoomFactor) / size) -
508                                   (aPixels.bottom ? 1 : 0)};
509
510   // Go through all the tiles in the map, find out if to draw them and do so.
511   for (var x = Math.floor(xMin / size); x < Math.ceil(xMax / size); x++) {
512     for (var y = Math.floor(yMin / size); y < Math.ceil(yMax / size); y++) { // slow script warnings on the tablet appear here!
513       // Only go to the drawing step if we need to draw this tile.
514       if (x < tiles.left || x > tiles.right ||
515           y < tiles.top || y > tiles.bottom) {
516         // Round here is **CRUCIAL** otherwise the images are filtered
517         // and the performance sucks (more than expected).
518         var xoff = Math.round((x * size - xMin) / gZoomFactor);
519         var yoff = Math.round((y * size - yMin) / gZoomFactor);
520         // Draw placeholder tile unless we overdraw.
521         if (!aOverdraw &&
522             (x < tiles.left -1  || x > tiles.right + 1 ||
523              y < tiles.top -1 || y > tiles.bottom + 1))
524           gMapContext.drawImage(gLoadingTile, xoff, yoff);
525
526         // Initiate loading/drawing of the actual tile.
527         gTileService.get(gActiveMap, {x: x, y: y, z: gPos.z},
528                          function(aImage, aStyle, aCoords) {
529           // Only draw if this applies for the current view.
530           if ((aStyle == gActiveMap) && (aCoords.z == gPos.z)) {
531             var ixMin = gPos.x - wid / 2;
532             var iyMin = gPos.y - ht / 2;
533             var ixoff = Math.round((aCoords.x * size - ixMin) / gZoomFactor);
534             var iyoff = Math.round((aCoords.y * size - iyMin) / gZoomFactor);
535             var URL = window.URL;
536             var imgURL = URL.createObjectURL(aImage);
537             var imgObj = new Image();
538             imgObj.src = imgURL;
539             imgObj.onload = function() {
540               gMapContext.drawImage(imgObj, ixoff, iyoff);
541               URL.revokeObjectURL(imgURL);
542             }
543           }
544         });
545       }
546     }
547   }
548   drawTrack();
549 }
550
551 function drawTrack() {
552   gLastDrawnPoint = null;
553   gCurPosMapCache = undefined;
554   gTrackContext.clearRect(0, 0, gTrackCanvas.width, gTrackCanvas.height);
555   if (gTrack.length) {
556     for (var i = 0; i < gTrack.length; i++) {
557       drawTrackPoint(gTrack[i].coords.latitude, gTrack[i].coords.longitude,
558                      (i + 1 >= gTrack.length));
559     }
560   }
561 }
562
563 function drawTrackPoint(aLatitude, aLongitude, lastPoint) {
564   var trackpoint = gps2xy(aLatitude, aLongitude);
565   // lastPoint is for optimizing (not actually executing the draw until the last)
566   trackpoint.optimized = (lastPoint === false);
567   var mappos = {x: Math.round((trackpoint.x - gPos.x) / gZoomFactor + gMapCanvas.width / 2),
568                 y: Math.round((trackpoint.y - gPos.y) / gZoomFactor + gMapCanvas.height / 2)};
569
570   if (!gLastDrawnPoint || !gLastDrawnPoint.optimized) {
571     gTrackContext.strokeStyle = gTrackColor;
572     gTrackContext.fillStyle = gTrackContext.strokeStyle;
573     gTrackContext.lineWidth = gTrackWidth;
574     gTrackContext.lineCap = "round";
575     gTrackContext.lineJoin = "round";
576   }
577   if (!gLastDrawnPoint || gLastDrawnPoint == trackpoint) {
578     // This breaks optimiziation, so make sure to close path and reset optimization.
579     if (gLastDrawnPoint && gLastDrawnPoint.optimized)
580       gTrackContext.stroke();
581     gTrackContext.beginPath();
582     trackpoint.optimized = false;
583     gTrackContext.arc(mappos.x, mappos.y,
584                       gTrackContext.lineWidth, 0, Math.PI * 2, false);
585     gTrackContext.fill();
586   }
587   else {
588     if (!gLastDrawnPoint || !gLastDrawnPoint.optimized) {
589       gTrackContext.beginPath();
590       gTrackContext.moveTo(Math.round((gLastDrawnPoint.x - gPos.x) / gZoomFactor + gMapCanvas.width / 2),
591                            Math.round((gLastDrawnPoint.y - gPos.y) / gZoomFactor + gMapCanvas.height / 2));
592     }
593     gTrackContext.lineTo(mappos.x, mappos.y);
594     if (!trackpoint.optimized)
595       gTrackContext.stroke();
596   }
597   gLastDrawnPoint = trackpoint;
598 }
599
600 function drawCurrentLocation(trackPoint) {
601   var locpoint = gps2xy(trackPoint.coords.latitude, trackPoint.coords.longitude);
602   var circleRadius = Math.round(gCurLocSize / 2);
603   var mappos = {x: Math.round((locpoint.x - gPos.x) / gZoomFactor + gMapCanvas.width / 2),
604                 y: Math.round((locpoint.y - gPos.y) / gZoomFactor + gMapCanvas.height / 2)};
605
606   undrawCurrentLocation();
607
608   // Cache overdrawn area.
609   gCurPosMapCache =
610       {point: locpoint,
611        radius: circleRadius,
612        data: gTrackContext.getImageData(mappos.x - circleRadius,
613                                         mappos.y - circleRadius,
614                                         circleRadius * 2, circleRadius * 2)};
615
616   gTrackContext.strokeStyle = gCurLocColor;
617   gTrackContext.fillStyle = gTrackContext.strokeStyle;
618   gTrackContext.beginPath();
619   gTrackContext.arc(mappos.x, mappos.y,
620                     circleRadius, 0, Math.PI * 2, false);
621   gTrackContext.fill();
622 }
623
624 function undrawCurrentLocation() {
625   if (gCurPosMapCache) {
626     var oldpoint = gCurPosMapCache.point;
627     var oldmp = {x: Math.round((oldpoint.x - gPos.x) / gZoomFactor + gMapCanvas.width / 2),
628                  y: Math.round((oldpoint.y - gPos.y) / gZoomFactor + gMapCanvas.height / 2)};
629     gTrackContext.putImageData(gCurPosMapCache.data,
630                                oldmp.x - gCurPosMapCache.radius,
631                                oldmp.y - gCurPosMapCache.radius);
632     gCurPosMapCache = undefined;
633   }
634 }
635
636 var mapEvHandler = {
637   handleEvent: function(aEvent) {
638     var touchEvent = aEvent.type.indexOf('touch') != -1;
639
640     if (touchEvent) {
641       aEvent.stopPropagation();
642     }
643
644     // Bail out if the event is happening on an input.
645     if (aEvent.target.tagName.toLowerCase() == "input")
646       return;
647
648     // Bail out on unwanted map moves, but not zoom or keyboard events.
649     if (aEvent.type.indexOf("mouse") === 0 || aEvent.type.indexOf("touch") === 0) {
650       // Bail out if this is neither a touch nor left-click.
651       if (!touchEvent && aEvent.button != 0)
652         return;
653
654       // Bail out if the started touch can't be found.
655       if (touchEvent && gDragging &&
656           !aEvent.changedTouches.identifiedTouch(gDragTouchID))
657         return;
658     }
659
660     var coordObj = touchEvent ?
661                    aEvent.changedTouches.identifiedTouch(gDragTouchID) :
662                    aEvent;
663
664     switch (aEvent.type) {
665       case "mousedown":
666       case "touchstart":
667         if (touchEvent) {
668           if (aEvent.targetTouches.length == 2) {
669             gPinchStartWidth = Math.sqrt(
670                 Math.pow(aEvent.targetTouches.item(1).clientX -
671                          aEvent.targetTouches.item(0).clientX, 2) +
672                 Math.pow(aEvent.targetTouches.item(1).clientY -
673                          aEvent.targetTouches.item(0).clientY, 2)
674             );
675           }
676           gDragTouchID = aEvent.changedTouches.item(0).identifier;
677           coordObj = aEvent.changedTouches.identifiedTouch(gDragTouchID);
678         }
679         var x = coordObj.clientX - gMapCanvas.offsetLeft;
680         var y = coordObj.clientY - gMapCanvas.offsetTop;
681
682         if (touchEvent || aEvent.button === 0) {
683           gDragging = true;
684         }
685         gLastMouseX = x;
686         gLastMouseY = y;
687         showUI();
688         break;
689       case "mousemove":
690       case "touchmove":
691         if (touchEvent && aEvent.targetTouches.length == 2) {
692           curPinchStartWidth = Math.sqrt(
693               Math.pow(aEvent.targetTouches.item(1).clientX -
694                        aEvent.targetTouches.item(0).clientX, 2) +
695               Math.pow(aEvent.targetTouches.item(1).clientY -
696                        aEvent.targetTouches.item(0).clientY, 2)
697           );
698           if (!gPinchStartWidth)
699             gPinchStartWidth = curPinchStartWidth;
700
701           if (gPinchStartWidth / curPinchStartWidth > 1.7 ||
702               gPinchStartWidth / curPinchStartWidth < 0.6) {
703             var newZoomLevel = gPos.z + (gPinchStartWidth < curPinchStartWidth ? 1 : -1);
704             if ((newZoomLevel >= 0) && (newZoomLevel <= gMaxZoom)) {
705               // Calculate new center of the map - preserve middle of pinch.
706               // This means that pixel distance between old center and middle
707               // must equal pixel distance of new center and middle.
708               var x = (aEvent.targetTouches.item(1).clientX +
709                        aEvent.targetTouches.item(0).clientX) / 2 -
710                       gMapCanvas.offsetLeft;
711               var y = (aEvent.targetTouches.item(1).clientY +
712                        aEvent.targetTouches.item(0).clientY) / 2 -
713                       gMapCanvas.offsetTop;
714
715               // Zoom factor after this action.
716               var newZoomFactor = Math.pow(2, gMaxZoom - newZoomLevel);
717               gPos.x -= (x - gMapCanvas.width / 2) * (newZoomFactor - gZoomFactor);
718               gPos.y -= (y - gMapCanvas.height / 2) * (newZoomFactor - gZoomFactor);
719
720               if (gPinchStartWidth < curPinchStartWidth)
721                 zoomIn();
722               else
723                 zoomOut();
724
725               // Reset pinch start width and start another pinch gesture.
726               gPinchStartWidth = null;
727             }
728           }
729           // If we are in a pinch, do not drag.
730           break;
731         }
732         var x = coordObj.clientX - gMapCanvas.offsetLeft;
733         var y = coordObj.clientY - gMapCanvas.offsetTop;
734         if (gDragging === true) {
735           var dX = x - gLastMouseX;
736           var dY = y - gLastMouseY;
737           gPos.x -= dX * gZoomFactor;
738           gPos.y -= dY * gZoomFactor;
739           if (true) { // use optimized path
740             var mapData = gMapContext.getImageData(0, 0,
741                                                    gMapCanvas.width,
742                                                    gMapCanvas.height);
743             gMapContext.clearRect(0, 0, gMapCanvas.width, gMapCanvas.height);
744             gMapContext.putImageData(mapData, dX, dY);
745             drawMap({left: (dX > 0) ? dX : 0,
746                      right: (dX < 0) ? -dX : 0,
747                      top: (dY > 0) ? dY : 0,
748                      bottom: (dY < 0) ? -dY : 0});
749           }
750           else {
751             drawMap(false, true);
752           }
753           showUI();
754         }
755         gLastMouseX = x;
756         gLastMouseY = y;
757         break;
758       case "mouseup":
759       case "touchend":
760         gPinchStartWidth = null;
761         gDragging = false;
762         showUI();
763         break;
764       case "mouseout":
765       case "touchcancel":
766       case "touchleave":
767         //gDragging = false;
768         break;
769       case "wheel":
770         // If we'd want pixels, we'd need to calc up using aEvent.deltaMode.
771         // See https://developer.mozilla.org/en-US/docs/Mozilla_event_reference/wheel
772
773         // Only accept (non-null) deltaY values
774         if (!aEvent.deltaY)
775           break;
776
777         // Debug output: "coordinates" of the point the mouse was over.
778         /*
779         var ptCoord = {x: gPos.x + (x - gMapCanvas.width / 2) * gZoomFactor,
780                        y: gPos.y + (x - gMapCanvas.height / 2) * gZoomFactor};
781         var gpsCoord = xy2gps(ptCoord.x, ptCoord.y);
782         var pt2Coord = gps2xy(gpsCoord.latitude, gpsCoord.longitude);
783         console.log(ptCoord.x + "/" + ptCoord.y + " - " +
784                     gpsCoord.latitude + "/" + gpsCoord.longitude + " - " +
785                     pt2Coord.x + "/" + pt2Coord.y);
786         */
787
788         var newZoomLevel = gPos.z + (aEvent.deltaY < 0 ? 1 : -1);
789         if ((newZoomLevel >= 0) && (newZoomLevel <= gMaxZoom)) {
790           // Calculate new center of the map - same point stays under the mouse.
791           // This means that the pixel distance between the old center and point
792           // must equal the pixel distance of the new center and that point.
793           var x = coordObj.clientX - gMapCanvas.offsetLeft;
794           var y = coordObj.clientY - gMapCanvas.offsetTop;
795
796           // Zoom factor after this action.
797           var newZoomFactor = Math.pow(2, gMaxZoom - newZoomLevel);
798           gPos.x -= (x - gMapCanvas.width / 2) * (newZoomFactor - gZoomFactor);
799           gPos.y -= (y - gMapCanvas.height / 2) * (newZoomFactor - gZoomFactor);
800
801           if (aEvent.deltaY < 0)
802             zoomIn();
803           else
804             zoomOut();
805         }
806         break;
807       case "keydown":
808         // Allow keyboard control to move and zoom the map.
809         // Should use aEvent.key instead of aEvent.which but needs bug 680830.
810         // See https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/keydown
811         var dX = 0;
812         var dY = 0;
813         switch (aEvent.which) {
814           case 39: // right
815             dX = -gTileSize / 2;
816           break;
817           case 37: // left
818             dX = gTileSize / 2;
819           break;
820           case 38: // up
821             dY = gTileSize / 2;
822           break;
823           case 40: // down
824             dY = -gTileSize / 2;
825           break;
826           case 87: // w
827           case 107: // + (numpad)
828           case 171: // + (normal key)
829             zoomIn();
830           break;
831           case 83: // s
832           case 109: // - (numpad)
833           case 173: // - (normal key)
834             zoomOut();
835           break;
836           case 48: // 0
837           case 49: // 1
838           case 50: // 2
839           case 51: // 3
840           case 52: // 4
841           case 53: // 5
842           case 54: // 6
843           case 55: // 7
844           case 56: // 8
845             zoomTo(aEvent.which - 38);
846           break;
847           case 57: // 9
848             zoomTo(9);
849           break;
850           case 96: // 0 (numpad)
851           case 97: // 1 (numpad)
852           case 98: // 2 (numpad)
853           case 99: // 3 (numpad)
854           case 100: // 4 (numpad)
855           case 101: // 5 (numpad)
856           case 102: // 6 (numpad)
857           case 103: // 7 (numpad)
858           case 104: // 8 (numpad)
859             zoomTo(aEvent.which - 86);
860           break;
861           case 105: // 9 (numpad)
862             zoomTo(9);
863           break;
864           default: // not supported
865             console.log("key not supported: " + aEvent.which);
866           break;
867         }
868
869         // Move if needed.
870         if (dX || dY) {
871           gPos.x -= dX * gZoomFactor;
872           gPos.y -= dY * gZoomFactor;
873           if (true) { // use optimized path
874             var mapData = gMapContext.getImageData(0, 0,
875                                                    gMapCanvas.width,
876                                                    gMapCanvas.height);
877             gMapContext.clearRect(0, 0, gMapCanvas.width, gMapCanvas.height);
878             gMapContext.putImageData(mapData, dX, dY);
879             drawMap({left: (dX > 0) ? dX : 0,
880                      right: (dX < 0) ? -dX : 0,
881                      top: (dY > 0) ? dY : 0,
882                      bottom: (dY < 0) ? -dY : 0});
883           }
884           else {
885             drawMap(false, true);
886           }
887         }
888         break;
889     }
890   }
891 };
892
893 var geofake = {
894   tracking: false,
895   lastPos: {x: undefined, y: undefined},
896   watchPosition: function(aSuccessCallback, aErrorCallback, aPrefObject) {
897     this.tracking = true;
898     var watchCall = function() {
899       // calc new position in lat/lon degrees
900       // 90° on Earth surface are ~10,000 km at the equator,
901       // so try moving at most 10m at a time
902       if (geofake.lastPos.x)
903         geofake.lastPos.x += (Math.random() - .5) * 90 / 1000000
904       else
905         geofake.lastPos.x = 48.208174
906       if (geofake.lastPos.y)
907         geofake.lastPos.y += (Math.random() - .5) * 90 / 1000000
908       else
909         geofake.lastPos.y = 16.373819
910       aSuccessCallback({timestamp: Date.now(),
911                         coords: {latitude: geofake.lastPos.x,
912                                  longitude: geofake.lastPos.y,
913                                  accuracy: 20}});
914       if (geofake.tracking)
915         setTimeout(watchCall, 1000);
916     };
917     setTimeout(watchCall, 1000);
918     return "foo";
919   },
920   clearWatch: function(aID) {
921     this.tracking = false;
922   }
923 }
924
925 function setCentering(aCheckbox) {
926   if (gMapPrefsLoaded && mainDB)
927     gPrefs.set("center_map", aCheckbox.checked);
928   gCenterPosition = aCheckbox.checked;
929 }
930
931 function setTracking(aCheckbox) {
932   if (gMapPrefsLoaded && mainDB)
933     gPrefs.set("tracking_enabled", aCheckbox.checked);
934   if (aCheckbox.checked)
935     startTracking();
936   else
937     endTracking();
938 }
939
940 function startTracking() {
941   if (gGeolocation) {
942     gActionLabel.textContent = "Establishing Position";
943     gAction.style.display = "block";
944     gGeoWatchID = gGeolocation.watchPosition(
945       function(position) {
946         if (gActionLabel.textContent) {
947           gActionLabel.textContent = "";
948           gAction.style.display = "none";
949         }
950         // Coords spec: https://developer.mozilla.org/en/XPCOM_Interface_Reference/NsIDOMGeoPositionCoords
951         var tPoint = {time: position.timestamp,
952                       coords: {latitude: position.coords.latitude,
953                                longitude: position.coords.longitude,
954                                altitude: position.coords.altitude,
955                                accuracy: position.coords.accuracy,
956                                altitudeAccuracy: position.coords.altitudeAccuracy,
957                                heading: position.coords.heading,
958                                speed: position.coords.speed},
959                       beginSegment: !gLastTrackPoint};
960         // Only add point to track is accuracy is good enough.
961         if (tPoint.coords.accuracy < gMinTrackAccuracy) {
962           gLastTrackPoint = tPoint;
963           gTrack.push(tPoint);
964           try { gTrackStore.push(tPoint); } catch(e) {}
965           var redrawn = false;
966           if (gCenterPosition) {
967             var posCoord = gps2xy(position.coords.latitude,
968                                   position.coords.longitude);
969             if (Math.abs(gPos.x - posCoord.x) > gMapCanvas.width * gZoomFactor / 4 ||
970                 Math.abs(gPos.y - posCoord.y) > gMapCanvas.height * gZoomFactor / 4) {
971               gPos.x = posCoord.x;
972               gPos.y = posCoord.y;
973               drawMap(); // This draws the current point as well.
974               redrawn = true;
975             }
976           }
977           if (!redrawn)
978             undrawCurrentLocation();
979             drawTrackPoint(position.coords.latitude, position.coords.longitude, true);
980         }
981         drawCurrentLocation(tPoint);
982       },
983       function(error) {
984         // Ignore erros for the moment, but this is good for debugging.
985         // See https://developer.mozilla.org/en/Using_geolocation#Handling_errors
986         if (gDebug)
987           console.log(error.message);
988       },
989       {enableHighAccuracy: true}
990     );
991   }
992 }
993
994 function endTracking() {
995   if (gActionLabel.textContent) {
996     gActionLabel.textContent = "";
997     gAction.style.display = "none";
998   }
999   if (gGeoWatchID) {
1000     gGeolocation.clearWatch(gGeoWatchID);
1001   }
1002 }
1003
1004 function clearTrack() {
1005   gTrack = [];
1006   gTrackStore.clear();
1007   drawTrack();
1008 }
1009
1010 var gTileService = {
1011   objStore: "tilecache",
1012
1013   ageLimit: 14 * 86400 * 1000, // 2 weeks (in ms)
1014
1015   get: function(aStyle, aCoords, aCallback) {
1016     var norm = normalizeCoords(aCoords);
1017     var dbkey = aStyle + "::" + norm.x + "," + norm.y + "," + norm.z;
1018     this.getDBCache(dbkey, function(aResult, aEvent) {
1019       if (aResult) {
1020         // We did get a cached object.
1021         aCallback(aResult.image, aStyle, aCoords);
1022         // Look at the timestamp and return if it's not too old.
1023         if (aResult.timestamp + gTileService.ageLimit > Date.now())
1024           return;
1025         // Reload cached tile otherwise.
1026         var oldDate = new Date(aResult.timestamp);
1027         console.log("reload cached tile: " + dbkey + " - " + oldDate.toUTCString());
1028       }
1029       // Retrieve image from the web and store it in the cache.
1030       var XHR = new XMLHttpRequest();
1031       XHR.open("GET",
1032                 gMapStyles[aStyle].url
1033                   .replace("{x}", norm.x)
1034                   .replace("{y}", norm.y)
1035                   .replace("{z}", norm.z)
1036                   .replace("[a-c]", String.fromCharCode(97 + Math.floor(Math.random() * 2)))
1037                   .replace("[1-4]", 1 + Math.floor(Math.random() * 3)),
1038                 true);
1039       XHR.responseType = "blob";
1040       XHR.addEventListener("load", function () {
1041         if (XHR.status === 200) {
1042           var blob = XHR.response;
1043           aCallback(blob, aStyle, aCoords);
1044           gTileService.setDBCache(dbkey, {image: blob, timestamp: Date.now()});
1045         }
1046       }, false);
1047       XHR.send();
1048     });
1049   },
1050
1051   getDBCache: function(aKey, aCallback) {
1052     if (!mainDB)
1053       return;
1054     var transaction = mainDB.transaction([this.objStore]);
1055     var request = transaction.objectStore(this.objStore).get(aKey);
1056     request.onsuccess = function(event) {
1057       aCallback(request.result, event);
1058     };
1059     request.onerror = function(event) {
1060       // Errors can be handled here.
1061       aCallback(undefined, event);
1062     };
1063   },
1064
1065   setDBCache: function(aKey, aValue, aCallback) {
1066     if (!mainDB)
1067       return;
1068     var success = false;
1069     var transaction = mainDB.transaction([this.objStore], "readwrite");
1070     var objStore = transaction.objectStore(this.objStore);
1071     var request = objStore.put(aValue, aKey);
1072     request.onsuccess = function(event) {
1073       success = true;
1074       if (aCallback)
1075         aCallback(success, event);
1076     };
1077     request.onerror = function(event) {
1078       // Errors can be handled here.
1079       if (aCallback)
1080         aCallback(success, event);
1081     };
1082   },
1083
1084   unsetDBCache: function(aKey, aCallback) {
1085     if (!mainDB)
1086       return;
1087     var success = false;
1088     var transaction = mainDB.transaction([this.objStore], "readwrite");
1089     var request = transaction.objectStore(this.objStore).delete(aKey);
1090     request.onsuccess = function(event) {
1091       success = true;
1092       if (aCallback)
1093         aCallback(success, event);
1094     };
1095     request.onerror = function(event) {
1096       // Errors can be handled here.
1097       if (aCallback)
1098         aCallback(success, event);
1099     }
1100   },
1101
1102   clearDB: function(aCallback) {
1103     if (!mainDB)
1104       return;
1105     var success = false;
1106     var transaction = mainDB.transaction([this.objStore], "readwrite");
1107     var request = transaction.objectStore(this.objStore).clear();
1108     request.onsuccess = function(event) {
1109       success = true;
1110       if (aCallback)
1111         aCallback(success, event);
1112     };
1113     request.onerror = function(event) {
1114       // Errors can be handled here.
1115       if (aCallback)
1116         aCallback(success, event);
1117     }
1118   }
1119 };