add zooming and map style selection
[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.0}; // This can be fractional if we are between zoom levels.
63
64 var gLastMouseX = 0;
65 var gLastMouseY = 0;
66
67 // Used as an assiciative array. They keys have to be strings, ours will be "xindex,yindex,zindex" e.g. "13,245,12".
68 var gTiles = {};
69
70 var gDragging = false;
71 var gZoomTouchID;
72
73 function initMap() {
74   gCanvas = document.getElementById("map");
75   gContext = gCanvas.getContext("2d");
76   if (!gActiveMap)
77     gActiveMap = "osm_mapnik";
78
79   gCanvas.addEventListener("mouseup", mapEvHandler, false);
80   gCanvas.addEventListener("mousemove", mapEvHandler, false);
81   gCanvas.addEventListener("mousedown", mapEvHandler, false);
82   gCanvas.addEventListener("mouseout", mapEvHandler, false);
83
84   gCanvas.addEventListener("touchstart", mapEvHandler, false);
85   gCanvas.addEventListener("touchmove", mapEvHandler, false);
86   gCanvas.addEventListener("touchend", mapEvHandler, false);
87   gCanvas.addEventListener("touchcancel", mapEvHandler, false);
88   gCanvas.addEventListener("touchleave", mapEvHandler, false);
89
90   gCanvas.addEventListener("DOMMouseScroll", mapEvHandler, false);
91   gCanvas.addEventListener("mousewheel", mapEvHandler, false);
92
93   document.getElementById("copyright").innerHTML =
94       gMapStyles[gActiveMap].copyright;
95 }
96
97 function resizeAndDraw() {
98   var viewportWidth = window.innerWidth;
99   var viewportHeight = window.innerHeight;
100
101   var canvasWidth = viewportWidth * 0.98;
102   var canvasHeight = (viewportHeight - 100) * 0.98;
103   gCanvas.style.position = "fixed";
104   gCanvas.width = canvasWidth;
105   gCanvas.height = canvasHeight;
106   drawMap();
107 }
108
109 function zoomIn() {
110   if (gPos.z < gMaxZoom) {
111     gPos.z++;
112     drawMap();
113   }
114 }
115
116 function zoomOut() {
117   if (gPos.z > 0) {
118     gPos.z--;
119     drawMap();
120   }
121 }
122
123 function setMapStyle() {
124   var mapSel = document.getElementById("mapSelector");
125   if (mapSel.selectedIndex >= 0 && gActiveMap != mapSel.value) {
126     gActiveMap = mapSel.value;
127     gTiles = {};
128     drawMap();
129   }
130 }
131
132 // A sane mod function that works for negative numbers.
133 // Returns a % b.
134 function mod(a, b) {
135   return ((a % b) + b) % b;
136 }
137
138 function normaliseIndices(x, y, z) {
139   return {x: mod(x, Math.pow(2, z)),
140           y: mod(y, Math.pow(2, z)),
141           z: z};
142 }
143
144 function tileURL(x, y, z) {
145   var norm = normaliseIndices(x, y, z);
146   return gMapStyles[gActiveMap].url.replace("{x}", norm.x)
147                                    .replace("{y}", norm.y)
148                                    .replace("{z}", norm.z);
149 }
150
151 // Returns true if the tile is outside the current view.
152 function isOutsideWindow(t) {
153   var pos = decodeIndex(t);
154   var x = pos[0];
155   var y = pos[1];
156   var z = pos[2];
157
158   var wid = gCanvas.width * Math.pow(2, gMaxZoom - z);
159   var ht = gCanvas.height * Math.pow(2, gMaxZoom - z);
160
161   x *= Math.pow(2, gMaxZoom - z);
162   y *= Math.pow(2, gMaxZoom - z);
163
164   var sz = gTileSize * Math.pow(2, gMaxZoom - z);
165   if (x > gPos.x + wid / 2 || y > gPos.y + ht / 2 ||
166       x + sz < gPos.x - wid / 2 || y - sz < gPos.y - ht / 2)
167     return true;
168   return false;
169 }
170
171 function encodeIndex(x, y, z) {
172   var norm = normaliseIndices(x, y, z);
173   return norm.x + "," + norm.y + "," + norm.z;
174 }
175
176 function decodeIndex(encodedIdx) {
177   return encodedIdx.split(",", 3);
178 }
179
180 function drawMap() {
181   // Go through all the currently loaded tiles. If we don't want any of them remove them.
182   // for (t in gTiles) {
183   //   if (isOutsideWindow(t))
184   //     delete gTiles[t];
185   // }
186   var z = Math.round(gPos.z);
187   var wid = gCanvas.width * Math.pow(2, gMaxZoom - z); // Width in level 18 pixels.
188   var ht = gCanvas.height * Math.pow(2, gMaxZoom - z); // Height in level 18 pixels.
189   var size = gTileSize * Math.pow(2, gMaxZoom - z); // Tile size in level 18 pixels.
190
191   var xMin = gPos.x - wid / 2; // Corners of the window in level 18 pixels.
192   var yMin = gPos.y - ht / 2;
193   var xMax = gPos.x + wid / 2;
194   var yMax = gPos.y + ht / 2;
195
196   // Go through all the tiles we want. If any of them aren't loaded or being loaded, do so.
197   for (var x = Math.floor(xMin / size); x < Math.ceil(xMax / size); x++) {
198     for (var y = Math.floor(yMin / size); y < Math.ceil(yMax / size); y++) {
199       var xoff = (x * size - xMin) / Math.pow(2, gMaxZoom - z);
200       var yoff = (y * size - yMin) / Math.pow(2, gMaxZoom - z);
201       var tileKey = encodeIndex(x, y, z);
202       if (gTiles[tileKey] && gTiles[tileKey].complete) {
203         // Round here is **CRUICIAL** otherwise the images are filtered and the performance sucks (more than expected).
204         gContext.drawImage(gTiles[tileKey], Math.round(xoff), Math.round(yoff));
205       }
206       else {
207         if (!gTiles[tileKey]) {
208           gTiles[tileKey] = new Image();
209           gTiles[tileKey].src = tileURL(x, y, gPos.z);
210           gTiles[tileKey].onload = function() {
211             // TODO: Just render this tile where it should be.
212             // context.drawImage(gTiles[tileKey], Math.round(xoff), Math.round(yoff)); // Doesn't work for some reason.
213             drawMap();
214           }
215         }
216         gContext.fillStyle = "#ffffff";
217         gContext.fillRect(Math.round(xoff), Math.round(yoff), gTileSize, gTileSize);
218       }
219     }
220   }
221 }
222
223 var mapEvHandler = {
224   handleEvent: function(aEvent) {
225     var touchEvent = aEvent.type.indexOf('touch') != -1;
226
227     // Bail out on unwanted map moves, but not mousewheel events.
228     if (aEvent.type != "DOMMouseScroll" && aEvent.type != "mousewheel") {
229       // Bail out if this is neither a touch nor left-click.
230       if (!touchEvent && aEvent.button != 0)
231         return;
232
233       // Bail out if the started touch can't be found.
234       if (touchEvent && zoomstart &&
235           !aEvent.changedTouches.identifiedTouch(gZoomTouchID))
236         return;
237     }
238
239     var coordObj = touchEvent ?
240                    aEvent.changedTouches.identifiedTouch(gZoomTouchID) :
241                    aEvent;
242
243     switch (aEvent.type) {
244       case "mousedown":
245       case "touchstart":
246         if (touchEvent) {
247           zoomTouchID = aEvent.changedTouches.item(0).identifier;
248           coordObj = aEvent.changedTouches.identifiedTouch(gZoomTouchID);
249         }
250         var x = coordObj.clientX - gCanvas.offsetLeft;
251         var y = coordObj.clientY - gCanvas.offsetTop;
252         if (touchEvent || aEvent.button === 0) {
253           gDragging = true;
254         }
255         gLastMouseX = x;
256         gLastMouseY = y;
257         break;
258       case "mousemove":
259       case "touchmove":
260         var x = coordObj.clientX - gCanvas.offsetLeft;
261         var y = coordObj.clientY - gCanvas.offsetTop;
262         if (gDragging === true) {
263           var dX = x - gLastMouseX;
264           var dY = y - gLastMouseY;
265           gPos.x -= dX * Math.pow(2, gMaxZoom - gPos.z);
266           gPos.y -= dY * Math.pow(2, gMaxZoom - gPos.z);
267           drawMap();
268         }
269         gLastMouseX = x;
270         gLastMouseY = y;
271         break;
272       case "mouseup":
273       case "touchend":
274         gDragging = false;
275         break;
276       case "mouseout":
277       case "touchcancel":
278       case "touchleave":
279         //gDragging = false;
280         break;
281       case "DOMMouseScroll":
282       case "mousewheel":
283         var delta = 0;
284         if (aEvent.wheelDelta) {
285           delta = aEvent.wheelDelta / 120;
286           if (window.opera)
287             delta = -delta;
288         }
289         else if (aEvent.detail) {
290           delta = -aEvent.detail / 3;
291         }
292
293         if (delta > 0)
294           zoomIn();
295         else if (delta < 0)
296           zoomOut();
297         break;
298     }
299   }
300 };