add icons, loading image and make mouse zooming keep the zoomed point at the same...
[lantea.git] / js / map.js
... / ...
CommitLineData
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
38var gCanvas, gContext;
39
40var gTileSize = 256;
41var gMaxZoom = 18; // The minimum is 0.
42
43var 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};
58var gActiveMap = "osm_mapnik";
59
60var 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
64var gLastMouseX = 0;
65var gLastMouseY = 0;
66var 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".
69var gTiles = {};
70var gLoadingTile;
71
72var gDragging = false;
73var gZoomTouchID;
74
75function 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
102function 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
114function zoomIn() {
115 if (gPos.z < gMaxZoom) {
116 gPos.z++;
117 drawMap();
118 }
119}
120
121function zoomOut() {
122 if (gPos.z > 0) {
123 gPos.z--;
124 drawMap();
125 }
126}
127
128function 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.
139function mod(a, b) {
140 return ((a % b) + b) % b;
141}
142
143function 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
150function 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.
158function 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
178function encodeIndex(x, y, z) {
179 var norm = normaliseIndices(x, y, z);
180 return norm.x + "," + norm.y + "," + norm.z;
181}
182
183function decodeIndex(encodedIdx) {
184 return encodedIdx.split(",", 3);
185}
186
187function 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
230var 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};