add icons, loading image and make mouse zooming keep the zoomed point at the same...
[lantea.git] / js / map.js
index 2a65f780749b9f0da9abae3edecc66aa2e3cff87..b1ae1a5f86ce8dc6032c5c3d19a340e12287349a 100644 (file)
--- a/js/map.js
+++ b/js/map.js
@@ -40,24 +40,43 @@ var gCanvas, gContext;
 var gTileSize = 256;
 var gMaxZoom = 18; // The minimum is 0.
 
-//drawMap();
+var gMapStyles = {
+  // OSM tile usage policy: http://wiki.openstreetmap.org/wiki/Tile_usage_policy
+  osm_mapnik:
+    {name: "OpenStreetMap (Mapnik)",
+     url: "http://tile.openstreetmap.org/{z}/{x}/{y}.png",
+     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>'},
+  osm_tilesathome:
+    {name: "OpenStreetMap (OSMarender)",
+     url: "http://tah.openstreetmap.org/Tiles/tile/{z}/{x}/{y}.png",
+     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>'},
+  mapquest_open:
+    {name: "MapQuest Open",
+     url: "http://otile1.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png",
+     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>.'}
+};
+var gActiveMap = "osm_mapnik";
 
 var gPos = {x: 35630000.0, // Current position in the map in pixels at the maximum zoom level (18)
             y: 23670000.0, // The range is 0-67108864 (2^gMaxZoom * gTileSize)
-            z: 5.0}; // This can be fractional if we are between zoom levels.
+            z: 5}; // This could be fractional if supported being between zoom levels.
 
 var gLastMouseX = 0;
 var gLastMouseY = 0;
+var gZoomFactor;
 
-// Used as an assiciative array. They keys have to be strings, ours will be "xindex,yindex,zindex" e.g. "13,245,12".
+// Used as an associative array. They keys have to be strings, ours will be "xindex,yindex,zindex" e.g. "13,245,12".
 var gTiles = {};
+var gLoadingTile;
 
 var gDragging = false;
 var gZoomTouchID;
 
-window.onload = function() {
+function initMap() {
   gCanvas = document.getElementById("map");
   gContext = gCanvas.getContext("2d");
+  if (!gActiveMap)
+    gActiveMap = "osm_mapnik";
 
   gCanvas.addEventListener("mouseup", mapEvHandler, false);
   gCanvas.addEventListener("mousemove", mapEvHandler, false);
@@ -73,11 +92,11 @@ window.onload = function() {
   gCanvas.addEventListener("DOMMouseScroll", mapEvHandler, false);
   gCanvas.addEventListener("mousewheel", mapEvHandler, false);
 
-  resizeAndDraw();
-}
+  document.getElementById("copyright").innerHTML =
+      gMapStyles[gActiveMap].copyright;
 
-window.onresize = function() {
-  resizeAndDraw();
+  gLoadingTile = new Image();
+  gLoadingTile.src = "style/loading.png";
 }
 
 function resizeAndDraw() {
@@ -85,7 +104,7 @@ function resizeAndDraw() {
   var viewportHeight = window.innerHeight;
 
   var canvasWidth = viewportWidth * 0.98;
-  var canvasHeight = (viewportHeight-110) * 0.98;
+  var canvasHeight = (viewportHeight - 100) * 0.98;
   gCanvas.style.position = "fixed";
   gCanvas.width = canvasWidth;
   gCanvas.height = canvasHeight;
@@ -106,6 +125,15 @@ function zoomOut() {
   }
 }
 
+function setMapStyle() {
+  var mapSel = document.getElementById("mapSelector");
+  if (mapSel.selectedIndex >= 0 && gActiveMap != mapSel.value) {
+    gActiveMap = mapSel.value;
+    gTiles = {};
+    drawMap();
+  }
+}
+
 // A sane mod function that works for negative numbers.
 // Returns a % b.
 function mod(a, b) {
@@ -113,15 +141,17 @@ function mod(a, b) {
 }
 
 function normaliseIndices(x, y, z) {
-  return {x: mod(x, Math.pow(2, z)),
-          y: mod(y, Math.pow(2, z)),
+  var zoomFactor = Math.pow(2, z);
+  return {x: mod(x, zoomFactor),
+          y: mod(y, zoomFactor),
           z: z};
 }
 
 function tileURL(x, y, z) {
   var norm = normaliseIndices(x, y, z);
-  var url = "http://tile.openstreetmap.org/" + norm.z + "/" + norm.x + "/" + norm.y + ".png";
-  return url;
+  return gMapStyles[gActiveMap].url.replace("{x}", norm.x)
+                                   .replace("{y}", norm.y)
+                                   .replace("{z}", norm.z);
 }
 
 // Returns true if the tile is outside the current view.
@@ -131,13 +161,14 @@ function isOutsideWindow(t) {
   var y = pos[1];
   var z = pos[2];
 
-  var wid = gCanvas.width * Math.pow(2, gMaxZoom - z);
-  var ht = gCanvas.height * Math.pow(2, gMaxZoom - z);
+  var zoomFactor = Math.pow(2, gMaxZoom - z);
+  var wid = gCanvas.width * zoomFactor;
+  var ht = gCanvas.height * zoomFactor;
 
-  x *= Math.pow(2, gMaxZoom - z);
-  y *= Math.pow(2, gMaxZoom - z);
+  x *= zoomFactor;
+  y *= zoomFactor;
 
-  var sz = gTileSize * Math.pow(2, gMaxZoom - z);
+  var sz = gTileSize * zoomFactor;
   if (x > gPos.x + wid / 2 || y > gPos.y + ht / 2 ||
       x + sz < gPos.x - wid / 2 || y - sz < gPos.y - ht / 2)
     return true;
@@ -159,10 +190,11 @@ function drawMap() {
   //   if (isOutsideWindow(t))
   //     delete gTiles[t];
   // }
-  var z = Math.round(gPos.z);
-  var wid = gCanvas.width * Math.pow(2, gMaxZoom - z); // Width in level 18 pixels.
-  var ht = gCanvas.height * Math.pow(2, gMaxZoom - z); // Height in level 18 pixels.
-  var sz = gTileSize * Math.pow(2, gMaxZoom - z); // Tile size in level 18 pixels.
+  document.getElementById("zoomLevel").textContent = gPos.z;
+  gZoomFactor = Math.pow(2, gMaxZoom - gPos.z);
+  var wid = gCanvas.width * gZoomFactor; // Width in level 18 pixels.
+  var ht = gCanvas.height * gZoomFactor; // Height in level 18 pixels.
+  var size = gTileSize * gZoomFactor; // Tile size in level 18 pixels.
 
   var xMin = gPos.x - wid / 2; // Corners of the window in level 18 pixels.
   var yMin = gPos.y - ht / 2;
@@ -170,11 +202,11 @@ function drawMap() {
   var yMax = gPos.y + ht / 2;
 
   // Go through all the tiles we want. If any of them aren't loaded or being loaded, do so.
-  for (var x = Math.floor(xMin / sz); x < Math.ceil(xMax / sz); ++x) {
-    for (var y = Math.floor(yMin / sz); y < Math.ceil(yMax / sz); ++y) {
-      var xoff = (x * sz - xMin) / Math.pow(2, gMaxZoom - z);
-      var yoff = (y * sz - yMin) / Math.pow(2, gMaxZoom - z);
-      var tileKey = encodeIndex(x, y, z);
+  for (var x = Math.floor(xMin / size); x < Math.ceil(xMax / size); x++) {
+    for (var y = Math.floor(yMin / size); y < Math.ceil(yMax / size); y++) {
+      var xoff = (x * size - xMin) / gZoomFactor;
+      var yoff = (y * size - yMin) / gZoomFactor;
+      var tileKey = encodeIndex(x, y, gPos.z);
       if (gTiles[tileKey] && gTiles[tileKey].complete) {
         // Round here is **CRUICIAL** otherwise the images are filtered and the performance sucks (more than expected).
         gContext.drawImage(gTiles[tileKey], Math.round(xoff), Math.round(yoff));
@@ -189,8 +221,7 @@ function drawMap() {
             drawMap();
           }
         }
-        gContext.fillStyle = "#ffffff";
-        gContext.fillRect(Math.round(xoff), Math.round(yoff), gTileSize, gTileSize);
+        gContext.drawImage(gLoadingTile, Math.round(xoff), Math.round(yoff));
       }
     }
   }
@@ -200,7 +231,7 @@ var mapEvHandler = {
   handleEvent: function(aEvent) {
     var touchEvent = aEvent.type.indexOf('touch') != -1;
 
-    // Bail out on unwanted map moves, but not mousewheel events.
+    // Bail out on unwanted map moves, but not zoom-changing events.
     if (aEvent.type != "DOMMouseScroll" && aEvent.type != "mousewheel") {
       // Bail out if this is neither a touch nor left-click.
       if (!touchEvent && aEvent.button != 0)
@@ -225,6 +256,7 @@ var mapEvHandler = {
         }
         var x = coordObj.clientX - gCanvas.offsetLeft;
         var y = coordObj.clientY - gCanvas.offsetTop;
+
         if (touchEvent || aEvent.button === 0) {
           gDragging = true;
         }
@@ -238,8 +270,8 @@ var mapEvHandler = {
         if (gDragging === true) {
           var dX = x - gLastMouseX;
           var dY = y - gLastMouseY;
-          gPos.x -= dX * Math.pow(2, gMaxZoom - gPos.z);
-          gPos.y -= dY * Math.pow(2, gMaxZoom - gPos.z);
+          gPos.x -= dX * gZoomFactor;
+          gPos.y -= dY * gZoomFactor;
           drawMap();
         }
         gLastMouseX = x;
@@ -266,6 +298,17 @@ var mapEvHandler = {
           delta = -aEvent.detail / 3;
         }
 
+        // Calculate new center of the map - same point stays under the mouse.
+        // This means that the pixel distance between the old center and point
+        // must equal the pixel distance of the new center and that point.
+        var x = coordObj.clientX - gCanvas.offsetLeft;
+        var y = coordObj.clientY - gCanvas.offsetTop;
+        // Zoom factor after this action.
+        var newZoomFactor = Math.pow(2, gMaxZoom - gPos.z + (delta > 0 ? -1 : 1));
+        gPos.x -= (x - gCanvas.width / 2) * (newZoomFactor - gZoomFactor);
+        gPos.y -= (y - gCanvas.height / 2) * (newZoomFactor - gZoomFactor);
+        document.getElementById("debug").textContent = newZoomFactor + " - " + gZoomFactor;
+
         if (delta > 0)
           zoomIn();
         else if (delta < 0)