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