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