X-Git-Url: https://git-public.kairo.at/?p=lantea.git;a=blobdiff_plain;f=js%2Fmap.js;h=cb659f9d2f6f81967557b57fca08fd39017d3de2;hp=2a65f780749b9f0da9abae3edecc66aa2e3cff87;hb=4b12da3a7069591d3ec6aceb4a949c97bc7549f2;hpb=23cd2dcceff47d3ff6156c6c4fba913f2ee2f9b9 diff --git a/js/map.js b/js/map.js index 2a65f78..cb659f9 100644 --- a/js/map.js +++ b/js/map.js @@ -1,63 +1,113 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Lantea mapping/tracking web app. - * - * The Initial Developer of the Original Code is - * Robert Kaiser . - * Portions created by the Initial Developer are Copyright (C) 2011 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Robert Kaiser - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -var gCanvas, gContext; +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var gCanvas, gContext, gGeolocation; +var gDebug = false; 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 + // Find some more OSM ones at http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers + osm_mapnik: + {name: "OpenStreetMap (Mapnik)", + url: "http://tile.openstreetmap.org/{z}/{x}/{y}.png", + copyright: 'Map data and imagery © OpenStreetMap contributors, CC-BY-SA'}, + osm_tilesathome: + {name: "OpenStreetMap (OSMarender)", + url: "http://tah.openstreetmap.org/Tiles/tile/{z}/{x}/{y}.png", + copyright: 'Map data and imagery © OpenStreetMap contributors, CC-BY-SA'}, + mapquest_open: + {name: "MapQuest OSM", + url: "http://otile1.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png", + copyright: 'Data, imagery and map information provided by MapQuest, OpenStreetMap and contributors, CC-BY-SA.'}, + mapquest_aerial: + {name: "MapQuest Open Aerial", + url: "http://oatile1.mqcdn.com/naip/{z}/{x}/{y}.png", + copyright: 'Data, imagery and map information provided by MapQuest, OpenStreetMap and contributors, CC-BY-SA.'}, + google_map: + {name: "Google Maps", + url: " http://mt1.google.com/vt/x={x}&y={y}&z={z}", + copyright: 'Map data and imagery © Google'}, +}; +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. +// The keys have to be strings, ours will be "xindex,yindex,zindex" e.g. "13,245,12". var gTiles = {}; +var gLoadingTile; + +var gMapPrefsLoaded = false; var gDragging = false; -var gZoomTouchID; +var gDragTouchID; + +var gGeoWatchID; +var gTrack = []; +var gLastTrackPoint; +var gCenterPosition = true; -window.onload = function() { +function initMap() { + gGeolocation = navigator.geolocation; gCanvas = document.getElementById("map"); gContext = gCanvas.getContext("2d"); + if (!gActiveMap) + gActiveMap = "osm_mapnik"; + + //gDebug = true; + if (gDebug) { + gGeolocation = geofake; + var hiddenList = document.getElementsByClassName("debugHide"); + // last to first - list of elements with that class is changing! + for (var i = hiddenList.length - 1; i >= 0; i--) { + hiddenList[i].classList.remove("debugHide"); + } + } + + var loopCnt = 0; + var getPersistentPrefs = function() { + if (mainDB) { + gPrefs.get("position", function(aValue) { + if (aValue) { + gPos = aValue; + drawMap(); + } + }); + gPrefs.get("center_map", function(aValue) { + if (aValue === undefined) + document.getElementById("centerCheckbox").checked = true; + else + document.getElementById("centerCheckbox").checked = aValue; + setCentering(document.getElementById("centerCheckbox")); + }); + gPrefs.get("tracking_enabled", function(aValue) { + if (aValue === undefined) + document.getElementById("trackCheckbox").checked = true; + else + document.getElementById("trackCheckbox").checked = aValue; + setTracking(document.getElementById("trackCheckbox")); + }); + gMapPrefsLoaded = true; + } + else + setTimeout(getPersistentPrefs, 100); + loopCnt++; + if (loopCnt > 20) { + gMapPrefsLoaded = true; + return; + } + }; + getPersistentPrefs(); gCanvas.addEventListener("mouseup", mapEvHandler, false); gCanvas.addEventListener("mousemove", mapEvHandler, false); @@ -73,11 +123,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 +135,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 +156,33 @@ function zoomOut() { } } +function gps2xy(aLatitude, aLongitude) { + var maxZoomFactor = Math.pow(2, gMaxZoom) * gTileSize; + var convLat = aLatitude * Math.PI / 180; + var rawY = (1 - Math.log(Math.tan(convLat) + + 1 / Math.cos(convLat)) / Math.PI) / 2 * maxZoomFactor; + var rawX = (aLongitude + 180) / 360 * maxZoomFactor; + return {x: Math.round(rawX), + y: Math.round(rawY)}; +} + +function xy2gps(aX, aY) { + var maxZoomFactor = Math.pow(2, gMaxZoom) * gTileSize; + var n = Math.PI - 2 * Math.PI * aY / maxZoomFactor; + return {latitude: 180 / Math.PI * + Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))), + longitude: aX / maxZoomFactor * 360 - 180}; +} + +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 +190,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 +210,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,24 +239,30 @@ 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; var xMax = gPos.x + wid / 2; 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); + if (gMapPrefsLoaded && mainDB) + gPrefs.set("position", gPos); + + // 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 / 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). + // Round here is **CRUCIAL** otherwise the images are filtered + // and the performance sucks (more than expected). gContext.drawImage(gTiles[tileKey], Math.round(xoff), Math.round(yoff)); } else { @@ -189,42 +275,70 @@ 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)); } } } + if (gTrack.length) + for (var i = 0; i < gTrack.length; i++) { + drawTrackPoint(gTrack[i].coords.latitude, gTrack[i].coords.longitude); + } +} + +function drawTrackPoint(aLatitude, aLongitude) { + var trackpoint = gps2xy(aLatitude, aLongitude); + gContext.strokeStyle = "#FF0000"; + gContext.fillStyle = gContext.strokeStyle; + gContext.lineWidth = 2; + gContext.lineCap = "round"; + gContext.lineJoin = "round"; + gContext.beginPath(); + if (!gLastTrackPoint || gLastTrackPoint == trackpoint) { + gContext.arc((trackpoint.x - gPos.x) / gZoomFactor + gCanvas.width / 2, + (trackpoint.y - gPos.y) / gZoomFactor + gCanvas.height / 2, + gContext.lineWidth, 0, Math.PI * 2, false); + gContext.fill(); + } + else { + gContext.moveTo((gLastTrackPoint.x - gPos.x) / gZoomFactor + gCanvas.width / 2, + (gLastTrackPoint.y - gPos.y) / gZoomFactor + gCanvas.height / 2); + gContext.lineTo((trackpoint.x - gPos.x) / gZoomFactor + gCanvas.width / 2, + (trackpoint.y - gPos.y) / gZoomFactor + gCanvas.height / 2); + gContext.stroke(); + } + gLastTrackPoint = trackpoint; } 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) return; // Bail out if the started touch can't be found. - if (touchEvent && zoomstart && - !aEvent.changedTouches.identifiedTouch(gZoomTouchID)) + if (touchEvent && gDragging && + !aEvent.changedTouches.identifiedTouch(gDragTouchID)) return; } var coordObj = touchEvent ? - aEvent.changedTouches.identifiedTouch(gZoomTouchID) : + aEvent.changedTouches.identifiedTouch(gDragTouchID) : aEvent; switch (aEvent.type) { case "mousedown": case "touchstart": if (touchEvent) { - zoomTouchID = aEvent.changedTouches.item(0).identifier; - coordObj = aEvent.changedTouches.identifiedTouch(gZoomTouchID); + gDragTouchID = aEvent.changedTouches.item(0).identifier; + coordObj = aEvent.changedTouches.identifiedTouch(gDragTouchID); } var x = coordObj.clientX - gCanvas.offsetLeft; var y = coordObj.clientY - gCanvas.offsetTop; + if (touchEvent || aEvent.button === 0) { gDragging = true; } @@ -238,8 +352,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 +380,27 @@ 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; + // Debug output: "coordinates" of the point the mouse was over. + /* + var ptCoord = {x: gPos.x + (x - gCanvas.width / 2) * gZoomFactor, + y: gPos.y + (x - gCanvas.height / 2) * gZoomFactor}; + var gpsCoord = xy2gps(ptCoord.x, ptCoord.y); + var pt2Coord = gps2xy(gpsCoord.latitude, gpsCoord.longitude); + document.getElementById("debug").textContent = + ptCoord.x + "/" + ptCoord.y + " - " + + gpsCoord.latitude + "/" + gpsCoord.longitude + " - " + + pt2Coord.x + "/" + pt2Coord.y; + */ + // 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); + if (delta > 0) zoomIn(); else if (delta < 0) @@ -274,3 +409,111 @@ var mapEvHandler = { } } }; + +var geofake = { + tracking: false, + lastPos: {x: undefined, y: undefined}, + watchPosition: function(aSuccessCallback, aErrorCallback, aPrefObject) { + this.tracking = true; + var watchCall = function() { + // calc new position in lat/lon degrees + // 90° on Earth surface are ~10,000 km at the equator, + // so try moving at most 10m at a time + if (geofake.lastPos.x) + geofake.lastPos.x += (Math.random() - .5) * 90 / 1000000 + else + geofake.lastPos.x = 48.208174 + if (geofake.lastPos.y) + geofake.lastPos.y += (Math.random() - .5) * 90 / 1000000 + else + geofake.lastPos.y = 16.373819 + aSuccessCallback({timestamp: Date.now(), + coords: {latitude: geofake.lastPos.x, + longitude: geofake.lastPos.y, + accuracy: 20}}); + if (geofake.tracking) + setTimeout(watchCall, 1000); + }; + setTimeout(watchCall, 1000); + return "foo"; + }, + clearWatch: function(aID) { + this.tracking = false; + } +} + +function setCentering(aCheckbox) { + if (gMapPrefsLoaded && mainDB) + gPrefs.set("center_map", aCheckbox.checked); + gCenterPosition = aCheckbox.checked; +} + +function setTracking(aCheckbox) { + if (gMapPrefsLoaded && mainDB) + gPrefs.set("tracking_enabled", aCheckbox.checked); + if (aCheckbox.checked) + startTracking(); + else + endTracking(); +} + +function startTracking() { + var loopCnt = 0; + var getStoredTrack = function() { + if (mainDB) + gTrackStore.getList(function(aTPoints) { + if (gDebug) + document.getElementById("debug").textContent = aTPoints.length + " points loaded."; + if (aTPoints.length) { + gTrack = aTPoints; + } + }); + else + setTimeout(getStoredTrack, 100); + loopCnt++; + if (loopCnt > 20) + return; + }; + getStoredTrack(); + if (navigator.geolocation) { + gGeoWatchID = gGeolocation.watchPosition( + function(position) { + // Coords spec: https://developer.mozilla.org/en/XPCOM_Interface_Reference/NsIDOMGeoPositionCoords + var tPoint = {time: position.timestamp, + coords: position.coords, + beginSegment: !gLastTrackPoint}; + gTrack.push(tPoint); + gTrackStore.push(tPoint); + drawTrackPoint(position.coords.latitude, position.coords.longitude); + if (gCenterPosition) { + var posCoord = gps2xy(position.coords.latitude, + position.coords.longitude); + if (Math.abs(gPos.x - posCoord.x) > gCanvas.width * gZoomFactor / 4 || + Math.abs(gPos.y - posCoord.y) > gCanvas.height * gZoomFactor / 4) { + gPos.x = posCoord.x; + gPos.y = posCoord.y; + drawMap(); + } + } + }, + function(error) { + // Ignore erros for the moment, but this is good for debugging. + // See https://developer.mozilla.org/en/Using_geolocation#Handling_errors + document.getElementById("debug").textContent = error.message; + }, + {enableHighAccuracy: true} + ); + } +} + +function endTracking() { + if (gGeoWatchID) { + gGeolocation.clearWatch(gGeoWatchID); + } +} + +function clearTrack() { + gTrack = []; + gTrackStore.clear(); + drawMap(); +}