b1ae1a5f86ce8dc6032c5c3d19a340e12287349a
[lantea.git] / js / map.js
1 /* ***** BEGIN LICENSE BLOCK *****
2  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3  *
4  * The contents of this file are subject to the Mozilla Public License Version
5  * 1.1 (the "License"); you may not use this file except in compliance with
6  * the License. You may obtain a copy of the License at
7  * http://www.mozilla.org/MPL/
8  *
9  * Software distributed under the License is distributed on an "AS IS" basis,
10  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11  * for the specific language governing rights and limitations under the
12  * License.
13  *
14  * The Original Code is Lantea mapping/tracking web app.
15  *
16  * The Initial Developer of the Original Code is
17  * Robert Kaiser <kairo@kairo.at>.
18  * Portions created by the Initial Developer are Copyright (C) 2011
19  * the Initial Developer. All Rights Reserved.
20  *
21  * Contributor(s):
22  *   Robert Kaiser <kairo@kairo.at>
23  *
24  * Alternatively, the contents of this file may be used under the terms of
25  * either the GNU General Public License Version 2 or later (the "GPL"), or
26  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27  * in which case the provisions of the GPL or the LGPL are applicable instead
28  * of those above. If you wish to allow use of your version of this file only
29  * under the terms of either the GPL or the LGPL, and not to allow others to
30  * use your version of this file under the terms of the MPL, indicate your
31  * decision by deleting the provisions above and replace them with the notice
32  * and other provisions required by the GPL or the LGPL. If you do not delete
33  * the provisions above, a recipient may use your version of this file under
34  * the terms of any one of the MPL, the GPL or the LGPL.
35  *
36  * ***** END LICENSE BLOCK ***** */
37
38 var gCanvas, gContext;
39
40 var gTileSize = 256;
41 var gMaxZoom = 18; // The minimum is 0.
42
43 var gMapStyles = {
44   // OSM tile usage policy: http://wiki.openstreetmap.org/wiki/Tile_usage_policy
45   osm_mapnik:
46     {name: "OpenStreetMap (Mapnik)",
47      url: "http://tile.openstreetmap.org/{z}/{x}/{y}.png",
48      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>'},
49   osm_tilesathome:
50     {name: "OpenStreetMap (OSMarender)",
51      url: "http://tah.openstreetmap.org/Tiles/tile/{z}/{x}/{y}.png",
52      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>'},
53   mapquest_open:
54     {name: "MapQuest Open",
55      url: "http://otile1.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png",
56      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>.'}
57 };
58 var gActiveMap = "osm_mapnik";
59
60 var gPos = {x: 35630000.0, // Current position in the map in pixels at the maximum zoom level (18)
61             y: 23670000.0, // The range is 0-67108864 (2^gMaxZoom * gTileSize)
62             z: 5}; // This could be fractional if supported being between zoom levels.
63
64 var gLastMouseX = 0;
65 var gLastMouseY = 0;
66 var gZoomFactor;
67
68 // Used as an associative array. They keys have to be strings, ours will be "xindex,yindex,zindex" e.g. "13,245,12".
69 var gTiles = {};
70 var gLoadingTile;
71
72 var gDragging = false;
73 var gZoomTouchID;
74
75 function initMap() {
76   gCanvas = document.getElementById("map");
77   gContext = gCanvas.getContext("2d");
78   if (!gActiveMap)
79     gActiveMap = "osm_mapnik";
80
81   gCanvas.addEventListener("mouseup", mapEvHandler, false);
82   gCanvas.addEventListener("mousemove", mapEvHandler, false);
83   gCanvas.addEventListener("mousedown", mapEvHandler, false);
84   gCanvas.addEventListener("mouseout", mapEvHandler, false);
85
86   gCanvas.addEventListener("touchstart", mapEvHandler, false);
87   gCanvas.addEventListener("touchmove", mapEvHandler, false);
88   gCanvas.addEventListener("touchend", mapEvHandler, false);
89   gCanvas.addEventListener("touchcancel", mapEvHandler, false);
90   gCanvas.addEventListener("touchleave", mapEvHandler, false);
91
92   gCanvas.addEventListener("DOMMouseScroll", mapEvHandler, false);
93   gCanvas.addEventListener("mousewheel", mapEvHandler, false);
94
95   document.getElementById("copyright").innerHTML =
96       gMapStyles[gActiveMap].copyright;
97
98   gLoadingTile = new Image();
99   gLoadingTile.src = "style/loading.png";
100 }
101
102 function resizeAndDraw() {
103   var viewportWidth = window.innerWidth;
104   var viewportHeight = window.innerHeight;
105
106   var canvasWidth = viewportWidth * 0.98;
107   var canvasHeight = (viewportHeight - 100) * 0.98;
108   gCanvas.style.position = "fixed";
109   gCanvas.width = canvasWidth;
110   gCanvas.height = canvasHeight;
111   drawMap();
112 }
113
114 function zoomIn() {
115   if (gPos.z < gMaxZoom) {
116     gPos.z++;
117     drawMap();
118   }
119 }
120
121 function zoomOut() {
122   if (gPos.z > 0) {
123     gPos.z--;
124     drawMap();
125   }
126 }
127
128 function setMapStyle() {
129   var mapSel = document.getElementById("mapSelector");
130   if (mapSel.selectedIndex >= 0 && gActiveMap != mapSel.value) {
131     gActiveMap = mapSel.value;
132     gTiles = {};
133     drawMap();
134   }
135 }
136
137 // A sane mod function that works for negative numbers.
138 // Returns a % b.
139 function mod(a, b) {
140   return ((a % b) + b) % b;
141 }
142
143 function normaliseIndices(x, y, z) {
144   var zoomFactor = Math.pow(2, z);
145   return {x: mod(x, zoomFactor),
146           y: mod(y, zoomFactor),
147           z: z};
148 }
149
150 function tileURL(x, y, z) {
151   var norm = normaliseIndices(x, y, z);
152   return gMapStyles[gActiveMap].url.replace("{x}", norm.x)
153                                    .replace("{y}", norm.y)
154                                    .replace("{z}", norm.z);
155 }
156
157 // Returns true if the tile is outside the current view.
158 function isOutsideWindow(t) {
159   var pos = decodeIndex(t);
160   var x = pos[0];
161   var y = pos[1];
162   var z = pos[2];
163
164   var zoomFactor = Math.pow(2, gMaxZoom - z);
165   var wid = gCanvas.width * zoomFactor;
166   var ht = gCanvas.height * zoomFactor;
167
168   x *= zoomFactor;
169   y *= zoomFactor;
170
171   var sz = gTileSize * zoomFactor;
172   if (x > gPos.x + wid / 2 || y > gPos.y + ht / 2 ||
173       x + sz < gPos.x - wid / 2 || y - sz < gPos.y - ht / 2)
174     return true;
175   return false;
176 }
177
178 function encodeIndex(x, y, z) {
179   var norm = normaliseIndices(x, y, z);
180   return norm.x + "," + norm.y + "," + norm.z;
181 }
182
183 function decodeIndex(encodedIdx) {
184   return encodedIdx.split(",", 3);
185 }
186
187 function drawMap() {
188   // Go through all the currently loaded tiles. If we don't want any of them remove them.
189   // for (t in gTiles) {
190   //   if (isOutsideWindow(t))
191   //     delete gTiles[t];
192   // }
193   document.getElementById("zoomLevel").textContent = gPos.z;
194   gZoomFactor = Math.pow(2, gMaxZoom - gPos.z);
195   var wid = gCanvas.width * gZoomFactor; // Width in level 18 pixels.
196   var ht = gCanvas.height * gZoomFactor; // Height in level 18 pixels.
197   var size = gTileSize * gZoomFactor; // Tile size in level 18 pixels.
198
199   var xMin = gPos.x - wid / 2; // Corners of the window in level 18 pixels.
200   var yMin = gPos.y - ht / 2;
201   var xMax = gPos.x + wid / 2;
202   var yMax = gPos.y + ht / 2;
203
204   // Go through all the tiles we want. If any of them aren't loaded or being loaded, do so.
205   for (var x = Math.floor(xMin / size); x < Math.ceil(xMax / size); x++) {
206     for (var y = Math.floor(yMin / size); y < Math.ceil(yMax / size); y++) {
207       var xoff = (x * size - xMin) / gZoomFactor;
208       var yoff = (y * size - yMin) / gZoomFactor;
209       var tileKey = encodeIndex(x, y, gPos.z);
210       if (gTiles[tileKey] && gTiles[tileKey].complete) {
211         // Round here is **CRUICIAL** otherwise the images are filtered and the performance sucks (more than expected).
212         gContext.drawImage(gTiles[tileKey], Math.round(xoff), Math.round(yoff));
213       }
214       else {
215         if (!gTiles[tileKey]) {
216           gTiles[tileKey] = new Image();
217           gTiles[tileKey].src = tileURL(x, y, gPos.z);
218           gTiles[tileKey].onload = function() {
219             // TODO: Just render this tile where it should be.
220             // context.drawImage(gTiles[tileKey], Math.round(xoff), Math.round(yoff)); // Doesn't work for some reason.
221             drawMap();
222           }
223         }
224         gContext.drawImage(gLoadingTile, Math.round(xoff), Math.round(yoff));
225       }
226     }
227   }
228 }
229
230 var mapEvHandler = {
231   handleEvent: function(aEvent) {
232     var touchEvent = aEvent.type.indexOf('touch') != -1;
233
234     // Bail out on unwanted map moves, but not zoom-changing events.
235     if (aEvent.type != "DOMMouseScroll" && aEvent.type != "mousewheel") {
236       // Bail out if this is neither a touch nor left-click.
237       if (!touchEvent && aEvent.button != 0)
238         return;
239
240       // Bail out if the started touch can't be found.
241       if (touchEvent && zoomstart &&
242           !aEvent.changedTouches.identifiedTouch(gZoomTouchID))
243         return;
244     }
245
246     var coordObj = touchEvent ?
247                    aEvent.changedTouches.identifiedTouch(gZoomTouchID) :
248                    aEvent;
249
250     switch (aEvent.type) {
251       case "mousedown":
252       case "touchstart":
253         if (touchEvent) {
254           zoomTouchID = aEvent.changedTouches.item(0).identifier;
255           coordObj = aEvent.changedTouches.identifiedTouch(gZoomTouchID);
256         }
257         var x = coordObj.clientX - gCanvas.offsetLeft;
258         var y = coordObj.clientY - gCanvas.offsetTop;
259
260         if (touchEvent || aEvent.button === 0) {
261           gDragging = true;
262         }
263         gLastMouseX = x;
264         gLastMouseY = y;
265         break;
266       case "mousemove":
267       case "touchmove":
268         var x = coordObj.clientX - gCanvas.offsetLeft;
269         var y = coordObj.clientY - gCanvas.offsetTop;
270         if (gDragging === true) {
271           var dX = x - gLastMouseX;
272           var dY = y - gLastMouseY;
273           gPos.x -= dX * gZoomFactor;
274           gPos.y -= dY * gZoomFactor;
275           drawMap();
276         }
277         gLastMouseX = x;
278         gLastMouseY = y;
279         break;
280       case "mouseup":
281       case "touchend":
282         gDragging = false;
283         break;
284       case "mouseout":
285       case "touchcancel":
286       case "touchleave":
287         //gDragging = false;
288         break;
289       case "DOMMouseScroll":
290       case "mousewheel":
291         var delta = 0;
292         if (aEvent.wheelDelta) {
293           delta = aEvent.wheelDelta / 120;
294           if (window.opera)
295             delta = -delta;
296         }
297         else if (aEvent.detail) {
298           delta = -aEvent.detail / 3;
299         }
300
301         // Calculate new center of the map - same point stays under the mouse.
302         // This means that the pixel distance between the old center and point
303         // must equal the pixel distance of the new center and that point.
304         var x = coordObj.clientX - gCanvas.offsetLeft;
305         var y = coordObj.clientY - gCanvas.offsetTop;
306         // Zoom factor after this action.
307         var newZoomFactor = Math.pow(2, gMaxZoom - gPos.z + (delta > 0 ? -1 : 1));
308         gPos.x -= (x - gCanvas.width / 2) * (newZoomFactor - gZoomFactor);
309         gPos.y -= (y - gCanvas.height / 2) * (newZoomFactor - gZoomFactor);
310         document.getElementById("debug").textContent = newZoomFactor + " - " + gZoomFactor;
311
312         if (delta > 0)
313           zoomIn();
314         else if (delta < 0)
315           zoomOut();
316         break;
317     }
318   }
319 };