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