wrap pushing to track store into a try/catch, make coords a plain object
[lantea.git] / js / map.js
... / ...
CommitLineData
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
5var gCanvas, gContext, gGeolocation;
6var gDebug = false;
7
8var gTileSize = 256;
9var gMaxZoom = 18; // The minimum is 0.
10
11var gMapStyles = {
12 // OSM tile usage policy: http://wiki.openstreetmap.org/wiki/Tile_usage_policy
13 // Find some more OSM ones at http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
14 osm_mapnik:
15 {name: "OpenStreetMap (Mapnik)",
16 url: "http://tile.openstreetmap.org/{z}/{x}/{y}.png",
17 copyright: 'Map data and imagery &copy; <a href="http://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'},
18 osm_tilesathome:
19 {name: "OpenStreetMap (OSMarender)",
20 url: "http://tah.openstreetmap.org/Tiles/tile/{z}/{x}/{y}.png",
21 copyright: 'Map data and imagery &copy; <a href="http://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'},
22 mapquest_open:
23 {name: "MapQuest OSM",
24 url: "http://otile1.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png",
25 copyright: 'Data, imagery and map information provided by MapQuest, <a href="http://www.openstreetmap.org/">OpenStreetMap</a> and contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>.'},
26 mapquest_aerial:
27 {name: "MapQuest Open Aerial",
28 url: "http://oatile1.mqcdn.com/naip/{z}/{x}/{y}.png",
29 copyright: 'Data, imagery and map information provided by MapQuest, <a href="http://www.openstreetmap.org/">OpenStreetMap</a> and contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>.'},
30 google_map:
31 {name: "Google Maps",
32 url: " http://mt1.google.com/vt/x={x}&y={y}&z={z}",
33 copyright: 'Map data and imagery &copy; <a href="http://maps.google.com/">Google</a>'},
34};
35var gActiveMap = "osm_mapnik";
36
37var gPos = {x: 35630000.0, // Current position in the map in pixels at the maximum zoom level (18)
38 y: 23670000.0, // The range is 0-67108864 (2^gMaxZoom * gTileSize)
39 z: 5}; // This could be fractional if supported being between zoom levels.
40
41var gLastMouseX = 0;
42var gLastMouseY = 0;
43var gZoomFactor;
44
45// Used as an associative array.
46// The keys have to be strings, ours will be "xindex,yindex,zindex" e.g. "13,245,12".
47var gTiles = {};
48var gLoadingTile;
49
50var gMapPrefsLoaded = false;
51
52var gDragging = false;
53var gDragTouchID;
54
55var gGeoWatchID;
56var gTrack = [];
57var gLastTrackPoint;
58var gCenterPosition = true;
59
60function initMap() {
61 gGeolocation = navigator.geolocation;
62 gCanvas = document.getElementById("map");
63 gContext = gCanvas.getContext("2d");
64 if (!gActiveMap)
65 gActiveMap = "osm_mapnik";
66
67 //gDebug = true;
68 if (gDebug) {
69 gGeolocation = geofake;
70 var hiddenList = document.getElementsByClassName("debugHide");
71 // last to first - list of elements with that class is changing!
72 for (var i = hiddenList.length - 1; i >= 0; i--) {
73 hiddenList[i].classList.remove("debugHide");
74 }
75 }
76
77 var loopCnt = 0;
78 var getPersistentPrefs = function() {
79 if (mainDB) {
80 gPrefs.get("position", function(aValue) {
81 if (aValue) {
82 gPos = aValue;
83 drawMap();
84 }
85 });
86 gPrefs.get("center_map", function(aValue) {
87 if (aValue === undefined)
88 document.getElementById("centerCheckbox").checked = true;
89 else
90 document.getElementById("centerCheckbox").checked = aValue;
91 setCentering(document.getElementById("centerCheckbox"));
92 });
93 gPrefs.get("tracking_enabled", function(aValue) {
94 if (aValue === undefined)
95 document.getElementById("trackCheckbox").checked = true;
96 else
97 document.getElementById("trackCheckbox").checked = aValue;
98 setTracking(document.getElementById("trackCheckbox"));
99 });
100 gMapPrefsLoaded = true;
101 }
102 else
103 setTimeout(getPersistentPrefs, 100);
104 loopCnt++;
105 if (loopCnt > 20) {
106 gMapPrefsLoaded = true;
107 return;
108 }
109 };
110 getPersistentPrefs();
111
112 gCanvas.addEventListener("mouseup", mapEvHandler, false);
113 gCanvas.addEventListener("mousemove", mapEvHandler, false);
114 gCanvas.addEventListener("mousedown", mapEvHandler, false);
115 gCanvas.addEventListener("mouseout", mapEvHandler, false);
116
117 gCanvas.addEventListener("touchstart", mapEvHandler, false);
118 gCanvas.addEventListener("touchmove", mapEvHandler, false);
119 gCanvas.addEventListener("touchend", mapEvHandler, false);
120 gCanvas.addEventListener("touchcancel", mapEvHandler, false);
121 gCanvas.addEventListener("touchleave", mapEvHandler, false);
122
123 gCanvas.addEventListener("DOMMouseScroll", mapEvHandler, false);
124 gCanvas.addEventListener("mousewheel", mapEvHandler, false);
125
126 document.getElementById("copyright").innerHTML =
127 gMapStyles[gActiveMap].copyright;
128
129 gLoadingTile = new Image();
130 gLoadingTile.src = "style/loading.png";
131}
132
133function resizeAndDraw() {
134 var viewportWidth = Math.min(window.innerWidth, window.outerWidth);
135 var viewportHeight = Math.min(window.innerHeight, window.outerHeight);
136
137 var canvasWidth = viewportWidth * 0.98;
138 var canvasHeight = (viewportHeight - 100) * 0.98;
139 gCanvas.style.position = "fixed";
140 gCanvas.width = canvasWidth;
141 gCanvas.height = canvasHeight;
142 drawMap();
143}
144
145function zoomIn() {
146 if (gPos.z < gMaxZoom) {
147 gPos.z++;
148 drawMap();
149 }
150}
151
152function zoomOut() {
153 if (gPos.z > 0) {
154 gPos.z--;
155 drawMap();
156 }
157}
158
159function gps2xy(aLatitude, aLongitude) {
160 var maxZoomFactor = Math.pow(2, gMaxZoom) * gTileSize;
161 var convLat = aLatitude * Math.PI / 180;
162 var rawY = (1 - Math.log(Math.tan(convLat) +
163 1 / Math.cos(convLat)) / Math.PI) / 2 * maxZoomFactor;
164 var rawX = (aLongitude + 180) / 360 * maxZoomFactor;
165 return {x: Math.round(rawX),
166 y: Math.round(rawY)};
167}
168
169function xy2gps(aX, aY) {
170 var maxZoomFactor = Math.pow(2, gMaxZoom) * gTileSize;
171 var n = Math.PI - 2 * Math.PI * aY / maxZoomFactor;
172 return {latitude: 180 / Math.PI *
173 Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))),
174 longitude: aX / maxZoomFactor * 360 - 180};
175}
176
177function setMapStyle() {
178 var mapSel = document.getElementById("mapSelector");
179 if (mapSel.selectedIndex >= 0 && gActiveMap != mapSel.value) {
180 gActiveMap = mapSel.value;
181 gTiles = {};
182 drawMap();
183 }
184}
185
186// A sane mod function that works for negative numbers.
187// Returns a % b.
188function mod(a, b) {
189 return ((a % b) + b) % b;
190}
191
192function normaliseIndices(x, y, z) {
193 var zoomFactor = Math.pow(2, z);
194 return {x: mod(x, zoomFactor),
195 y: mod(y, zoomFactor),
196 z: z};
197}
198
199function tileURL(x, y, z) {
200 var norm = normaliseIndices(x, y, z);
201 return gMapStyles[gActiveMap].url.replace("{x}", norm.x)
202 .replace("{y}", norm.y)
203 .replace("{z}", norm.z);
204}
205
206// Returns true if the tile is outside the current view.
207function isOutsideWindow(t) {
208 var pos = decodeIndex(t);
209 var x = pos[0];
210 var y = pos[1];
211 var z = pos[2];
212
213 var zoomFactor = Math.pow(2, gMaxZoom - z);
214 var wid = gCanvas.width * zoomFactor;
215 var ht = gCanvas.height * zoomFactor;
216
217 x *= zoomFactor;
218 y *= zoomFactor;
219
220 var sz = gTileSize * zoomFactor;
221 if (x > gPos.x + wid / 2 || y > gPos.y + ht / 2 ||
222 x + sz < gPos.x - wid / 2 || y - sz < gPos.y - ht / 2)
223 return true;
224 return false;
225}
226
227function encodeIndex(x, y, z) {
228 var norm = normaliseIndices(x, y, z);
229 return norm.x + "," + norm.y + "," + norm.z;
230}
231
232function decodeIndex(encodedIdx) {
233 return encodedIdx.split(",", 3);
234}
235
236function drawMap() {
237 // Go through all the currently loaded tiles. If we don't want any of them remove them.
238 // for (t in gTiles) {
239 // if (isOutsideWindow(t))
240 // delete gTiles[t];
241 // }
242 document.getElementById("zoomLevel").textContent = gPos.z;
243 gZoomFactor = Math.pow(2, gMaxZoom - gPos.z);
244 var wid = gCanvas.width * gZoomFactor; // Width in level 18 pixels.
245 var ht = gCanvas.height * gZoomFactor; // Height in level 18 pixels.
246 var size = gTileSize * gZoomFactor; // Tile size in level 18 pixels.
247
248 var xMin = gPos.x - wid / 2; // Corners of the window in level 18 pixels.
249 var yMin = gPos.y - ht / 2;
250 var xMax = gPos.x + wid / 2;
251 var yMax = gPos.y + ht / 2;
252
253 if (gMapPrefsLoaded && mainDB)
254 gPrefs.set("position", gPos);
255
256 // Go through all the tiles we want.
257 // If any of them aren't loaded or being loaded, do so.
258 for (var x = Math.floor(xMin / size); x < Math.ceil(xMax / size); x++) {
259 for (var y = Math.floor(yMin / size); y < Math.ceil(yMax / size); y++) {
260 var xoff = (x * size - xMin) / gZoomFactor;
261 var yoff = (y * size - yMin) / gZoomFactor;
262 var tileKey = encodeIndex(x, y, gPos.z);
263 if (gTiles[tileKey] && gTiles[tileKey].complete) {
264 // Round here is **CRUCIAL** otherwise the images are filtered
265 // and the performance sucks (more than expected).
266 gContext.drawImage(gTiles[tileKey], Math.round(xoff), Math.round(yoff));
267 }
268 else {
269 if (!gTiles[tileKey]) {
270 gTiles[tileKey] = new Image();
271 gTiles[tileKey].src = tileURL(x, y, gPos.z);
272 gTiles[tileKey].onload = function() {
273 // TODO: Just render this tile where it should be.
274 // context.drawImage(gTiles[tileKey], Math.round(xoff), Math.round(yoff)); // Doesn't work for some reason.
275 drawMap();
276 }
277 }
278 gContext.drawImage(gLoadingTile, Math.round(xoff), Math.round(yoff));
279 }
280 }
281 }
282 if (gTrack.length)
283 for (var i = 0; i < gTrack.length; i++) {
284 drawTrackPoint(gTrack[i].coords.latitude, gTrack[i].coords.longitude);
285 }
286}
287
288function drawTrackPoint(aLatitude, aLongitude) {
289 var trackpoint = gps2xy(aLatitude, aLongitude);
290 gContext.strokeStyle = "#FF0000";
291 gContext.fillStyle = gContext.strokeStyle;
292 gContext.lineWidth = 2;
293 gContext.lineCap = "round";
294 gContext.lineJoin = "round";
295 gContext.beginPath();
296 if (!gLastTrackPoint || gLastTrackPoint == trackpoint) {
297 gContext.arc((trackpoint.x - gPos.x) / gZoomFactor + gCanvas.width / 2,
298 (trackpoint.y - gPos.y) / gZoomFactor + gCanvas.height / 2,
299 gContext.lineWidth, 0, Math.PI * 2, false);
300 gContext.fill();
301 }
302 else {
303 gContext.moveTo((gLastTrackPoint.x - gPos.x) / gZoomFactor + gCanvas.width / 2,
304 (gLastTrackPoint.y - gPos.y) / gZoomFactor + gCanvas.height / 2);
305 gContext.lineTo((trackpoint.x - gPos.x) / gZoomFactor + gCanvas.width / 2,
306 (trackpoint.y - gPos.y) / gZoomFactor + gCanvas.height / 2);
307 gContext.stroke();
308 }
309 gLastTrackPoint = trackpoint;
310}
311
312var mapEvHandler = {
313 handleEvent: function(aEvent) {
314 var touchEvent = aEvent.type.indexOf('touch') != -1;
315
316 // Bail out on unwanted map moves, but not zoom-changing events.
317 if (aEvent.type != "DOMMouseScroll" && aEvent.type != "mousewheel") {
318 // Bail out if this is neither a touch nor left-click.
319 if (!touchEvent && aEvent.button != 0)
320 return;
321
322 // Bail out if the started touch can't be found.
323 if (touchEvent && gDragging &&
324 !aEvent.changedTouches.identifiedTouch(gDragTouchID))
325 return;
326 }
327
328 var coordObj = touchEvent ?
329 aEvent.changedTouches.identifiedTouch(gDragTouchID) :
330 aEvent;
331
332 switch (aEvent.type) {
333 case "mousedown":
334 case "touchstart":
335 if (touchEvent) {
336 gDragTouchID = aEvent.changedTouches.item(0).identifier;
337 coordObj = aEvent.changedTouches.identifiedTouch(gDragTouchID);
338 }
339 var x = coordObj.clientX - gCanvas.offsetLeft;
340 var y = coordObj.clientY - gCanvas.offsetTop;
341
342 if (touchEvent || aEvent.button === 0) {
343 gDragging = true;
344 }
345 gLastMouseX = x;
346 gLastMouseY = y;
347 break;
348 case "mousemove":
349 case "touchmove":
350 var x = coordObj.clientX - gCanvas.offsetLeft;
351 var y = coordObj.clientY - gCanvas.offsetTop;
352 if (gDragging === true) {
353 var dX = x - gLastMouseX;
354 var dY = y - gLastMouseY;
355 gPos.x -= dX * gZoomFactor;
356 gPos.y -= dY * gZoomFactor;
357 drawMap();
358 }
359 gLastMouseX = x;
360 gLastMouseY = y;
361 break;
362 case "mouseup":
363 case "touchend":
364 gDragging = false;
365 break;
366 case "mouseout":
367 case "touchcancel":
368 case "touchleave":
369 //gDragging = false;
370 break;
371 case "DOMMouseScroll":
372 case "mousewheel":
373 var delta = 0;
374 if (aEvent.wheelDelta) {
375 delta = aEvent.wheelDelta / 120;
376 if (window.opera)
377 delta = -delta;
378 }
379 else if (aEvent.detail) {
380 delta = -aEvent.detail / 3;
381 }
382
383 // Calculate new center of the map - same point stays under the mouse.
384 // This means that the pixel distance between the old center and point
385 // must equal the pixel distance of the new center and that point.
386 var x = coordObj.clientX - gCanvas.offsetLeft;
387 var y = coordObj.clientY - gCanvas.offsetTop;
388 // Debug output: "coordinates" of the point the mouse was over.
389 /*
390 var ptCoord = {x: gPos.x + (x - gCanvas.width / 2) * gZoomFactor,
391 y: gPos.y + (x - gCanvas.height / 2) * gZoomFactor};
392 var gpsCoord = xy2gps(ptCoord.x, ptCoord.y);
393 var pt2Coord = gps2xy(gpsCoord.latitude, gpsCoord.longitude);
394 document.getElementById("debug").textContent =
395 ptCoord.x + "/" + ptCoord.y + " - " +
396 gpsCoord.latitude + "/" + gpsCoord.longitude + " - " +
397 pt2Coord.x + "/" + pt2Coord.y;
398 */
399 // Zoom factor after this action.
400 var newZoomFactor = Math.pow(2, gMaxZoom - gPos.z + (delta > 0 ? -1 : 1));
401 gPos.x -= (x - gCanvas.width / 2) * (newZoomFactor - gZoomFactor);
402 gPos.y -= (y - gCanvas.height / 2) * (newZoomFactor - gZoomFactor);
403
404 if (delta > 0)
405 zoomIn();
406 else if (delta < 0)
407 zoomOut();
408 break;
409 }
410 }
411};
412
413var geofake = {
414 tracking: false,
415 lastPos: {x: undefined, y: undefined},
416 watchPosition: function(aSuccessCallback, aErrorCallback, aPrefObject) {
417 this.tracking = true;
418 var watchCall = function() {
419 // calc new position in lat/lon degrees
420 // 90° on Earth surface are ~10,000 km at the equator,
421 // so try moving at most 10m at a time
422 if (geofake.lastPos.x)
423 geofake.lastPos.x += (Math.random() - .5) * 90 / 1000000
424 else
425 geofake.lastPos.x = 48.208174
426 if (geofake.lastPos.y)
427 geofake.lastPos.y += (Math.random() - .5) * 90 / 1000000
428 else
429 geofake.lastPos.y = 16.373819
430 aSuccessCallback({timestamp: Date.now(),
431 coords: {latitude: geofake.lastPos.x,
432 longitude: geofake.lastPos.y,
433 accuracy: 20}});
434 if (geofake.tracking)
435 setTimeout(watchCall, 1000);
436 };
437 setTimeout(watchCall, 1000);
438 return "foo";
439 },
440 clearWatch: function(aID) {
441 this.tracking = false;
442 }
443}
444
445function setCentering(aCheckbox) {
446 if (gMapPrefsLoaded && mainDB)
447 gPrefs.set("center_map", aCheckbox.checked);
448 gCenterPosition = aCheckbox.checked;
449}
450
451function setTracking(aCheckbox) {
452 if (gMapPrefsLoaded && mainDB)
453 gPrefs.set("tracking_enabled", aCheckbox.checked);
454 if (aCheckbox.checked)
455 startTracking();
456 else
457 endTracking();
458}
459
460function startTracking() {
461 var loopCnt = 0;
462 var getStoredTrack = function() {
463 if (mainDB)
464 gTrackStore.getList(function(aTPoints) {
465 if (gDebug)
466 document.getElementById("debug").textContent = aTPoints.length + " points loaded.";
467 if (aTPoints.length) {
468 gTrack = aTPoints;
469 }
470 });
471 else
472 setTimeout(getStoredTrack, 100);
473 loopCnt++;
474 if (loopCnt > 20)
475 return;
476 };
477 getStoredTrack();
478 if (gGeolocation) {
479 gGeoWatchID = gGeolocation.watchPosition(
480 function(position) {
481 // Coords spec: https://developer.mozilla.org/en/XPCOM_Interface_Reference/NsIDOMGeoPositionCoords
482 var tPoint = {time: position.timestamp,
483 coords: {latitude: position.coords.latitude,
484 longitude: position.coords.longitude,
485 altitude: position.coords.altitude,
486 accuracy: position.coords.accuracy,
487 altitudeAccuracy: position.coords.altitudeAccuracy,
488 heading: position.coords.heading,
489 speed: position.coords.speed},
490 beginSegment: !gLastTrackPoint};
491 gTrack.push(tPoint);
492 try { gTrackStore.push(tPoint); } catch(e) {}
493 drawTrackPoint(position.coords.latitude, position.coords.longitude);
494 if (gCenterPosition) {
495 var posCoord = gps2xy(position.coords.latitude,
496 position.coords.longitude);
497 if (Math.abs(gPos.x - posCoord.x) > gCanvas.width * gZoomFactor / 4 ||
498 Math.abs(gPos.y - posCoord.y) > gCanvas.height * gZoomFactor / 4) {
499 gPos.x = posCoord.x;
500 gPos.y = posCoord.y;
501 drawMap();
502 }
503 }
504 },
505 function(error) {
506 // Ignore erros for the moment, but this is good for debugging.
507 // See https://developer.mozilla.org/en/Using_geolocation#Handling_errors
508 document.getElementById("debug").textContent = error.message;
509 },
510 {enableHighAccuracy: true}
511 );
512 }
513}
514
515function endTracking() {
516 if (gGeoWatchID) {
517 gGeolocation.clearWatch(gGeoWatchID);
518 }
519}
520
521function clearTrack() {
522 gTrack = [];
523 gTrackStore.clear();
524 drawMap();
525}