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