X-Git-Url: https://git-public.kairo.at/?p=lantea.git;a=blobdiff_plain;f=js%2Fmap.js;h=a8105ae48f0bbb0845162a32ec85bc97e2b655dd;hp=4cd4ea7b809ea786d1ca1dbdbd1ee8be86ab6e24;hb=b5e49b956629a45c3af14a87790060cbf18a718a;hpb=99631a7543656fb58fe3c7f7913773d49630db03 diff --git a/js/map.js b/js/map.js index 4cd4ea7..a8105ae 100644 --- a/js/map.js +++ b/js/map.js @@ -1,64 +1,46 @@ -/* ***** 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 gMapCanvas, gMapContext, gTrackCanvas, gTrackContext, gGeolocation; +var gDebug = false; var gTileSize = 256; var gMaxZoom = 18; // The minimum is 0. +var gMinTrackAccuracy = 1000; // meters +var gTrackWidth = 2; // pixels +var gTrackColor = "#FF0000"; +var gCurLocSize = 6; // pixels +var gCurLocColor = "#A00000"; + 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'}, + copyright: 'Map data and imagery © OpenStreetMap contributors, ODbL/CC-BY-SA'}, + osm_cyclemap: + {name: "Cycle Map (OSM)", + url: "http://[a-c].tile.opencyclemap.org/cycle/{z}/{x}/{y}.png", + copyright: 'Map data and imagery © OpenStreetMap contributors, ODbL/CC-BY-SA'}, + osm_transmap: + {name: "Transport Map (OSM)", + url: "http://[a-c].tile2.opencyclemap.org/transport/{z}/{x}/{y}.png", + copyright: 'Map data and imagery © OpenStreetMap contributors, ODbL/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.'}, + url: "http://otile[1-4].mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png", + copyright: 'Map data © OpenStreetMap and contributors (ODbL/CC-BY-SA), tiles Courtesy of MapQuest.'}, 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.'}, + url: "http://oatile[1-4].mqcdn.com/tiles/1.0.0/sat/{z}/{x}/{y}.jpg", + copyright: 'Tiles Courtesy of MapQuest, portions Courtesy NASA/JPL-Caltech and U.S. Depart. of Agriculture, Farm Service Agency.'}, + opengeoserver_arial: + {name: "OpenGeoServer Aerial", + url: "http://services.opengeoserver.org/tiles/1.0.0/globe.aerial_EPSG3857/{z}/{x}/{y}.png?origin=nw", + copyright: 'Tiles by OpenGeoServer.org, CC-BY 3.0 AT.'}, google_map: {name: "Google Maps", url: " http://mt1.google.com/vt/x={x}&y={y}&z={z}", @@ -74,57 +56,125 @@ var gLastMouseX = 0; var gLastMouseY = 0; var gZoomFactor; -// 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 gMapPrefsLoaded = false; + var gDragging = false; -var gZoomTouchID; +var gDragTouchID; var gGeoWatchID; var gTrack = []; -var gLastTrackPoint; +var gLastTrackPoint, gLastDrawnPoint; var gCenterPosition = true; +var gCurPosMapCache; + function initMap() { - gCanvas = document.getElementById("map"); - gContext = gCanvas.getContext("2d"); + gGeolocation = navigator.geolocation; + gMapCanvas = document.getElementById("map"); + gMapContext = gMapCanvas.getContext("2d"); + gTrackCanvas = document.getElementById("track"); + gTrackContext = gTrackCanvas.getContext("2d"); if (!gActiveMap) gActiveMap = "osm_mapnik"; - gCanvas.addEventListener("mouseup", mapEvHandler, false); - gCanvas.addEventListener("mousemove", mapEvHandler, false); - gCanvas.addEventListener("mousedown", mapEvHandler, false); - gCanvas.addEventListener("mouseout", mapEvHandler, false); + //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"); + } + } - gCanvas.addEventListener("touchstart", mapEvHandler, false); - gCanvas.addEventListener("touchmove", mapEvHandler, false); - gCanvas.addEventListener("touchend", mapEvHandler, false); - gCanvas.addEventListener("touchcancel", mapEvHandler, false); - gCanvas.addEventListener("touchleave", mapEvHandler, false); + var loopCnt = 0; + var getPersistentPrefs = function() { + if (mainDB) { + gWaitCounter++; + gPrefs.get("position", function(aValue) { + if (aValue) { + gPos = aValue; + gWaitCounter--; + } + }); + gWaitCounter++; + gPrefs.get("center_map", function(aValue) { + if (aValue === undefined) + document.getElementById("centerCheckbox").checked = true; + else + document.getElementById("centerCheckbox").checked = aValue; + setCentering(document.getElementById("centerCheckbox")); + gWaitCounter--; + }); + gWaitCounter++; + gPrefs.get("tracking_enabled", function(aValue) { + if (aValue === undefined) + document.getElementById("trackCheckbox").checked = true; + else + document.getElementById("trackCheckbox").checked = aValue; + gWaitCounter--; + }); + gWaitCounter++; + gTrackStore.getList(function(aTPoints) { + if (gDebug) + console.log(aTPoints.length + " points loaded."); + if (aTPoints.length) { + gTrack = aTPoints; + } + gWaitCounter--; + }); + } + else + setTimeout(getPersistentPrefs, 100); + loopCnt++; + if (loopCnt > 50) { + console.log("Loading prefs failed."); + } + }; + getPersistentPrefs(); + + gTrackCanvas.addEventListener("mouseup", mapEvHandler, false); + gTrackCanvas.addEventListener("mousemove", mapEvHandler, false); + gTrackCanvas.addEventListener("mousedown", mapEvHandler, false); + gTrackCanvas.addEventListener("mouseout", mapEvHandler, false); + + gTrackCanvas.addEventListener("touchstart", mapEvHandler, false); + gTrackCanvas.addEventListener("touchmove", mapEvHandler, false); + gTrackCanvas.addEventListener("touchend", mapEvHandler, false); + gTrackCanvas.addEventListener("touchcancel", mapEvHandler, false); + gTrackCanvas.addEventListener("touchleave", mapEvHandler, false); - gCanvas.addEventListener("DOMMouseScroll", mapEvHandler, false); - gCanvas.addEventListener("mousewheel", mapEvHandler, false); + // XXX deprecated? see https://groups.google.com/forum/?fromgroups#!topic/mozilla.dev.planning/kuhrORubaRY[1-25] + gTrackCanvas.addEventListener("DOMMouseScroll", mapEvHandler, false); + gTrackCanvas.addEventListener("mousewheel", mapEvHandler, false); document.getElementById("copyright").innerHTML = gMapStyles[gActiveMap].copyright; gLoadingTile = new Image(); gLoadingTile.src = "style/loading.png"; + gWaitCounter++; + gLoadingTile.onload = function() { gWaitCounter--; }; } function resizeAndDraw() { - var viewportWidth = window.innerWidth; - var viewportHeight = window.innerHeight; - - var canvasWidth = viewportWidth * 0.98; - var canvasHeight = (viewportHeight - 100) * 0.98; - gCanvas.style.position = "fixed"; - gCanvas.width = canvasWidth; - gCanvas.height = canvasHeight; - drawMap(); + var viewportWidth = Math.min(window.innerWidth, window.outerWidth); + var viewportHeight = Math.min(window.innerHeight, window.outerHeight); + if (gMapCanvas && gTrackCanvas) { + gMapCanvas.width = viewportWidth; + gMapCanvas.height = viewportHeight; + gTrackCanvas.width = viewportWidth; + gTrackCanvas.height = viewportHeight; + drawMap(); + showUI(); + } } +// Using scale(x, y) together with drawing old data on scaled canvas would be an improvement for zooming. +// See https://developer.mozilla.org/en-US/docs/Canvas_tutorial/Transformations#Scaling + function zoomIn() { if (gPos.z < gMaxZoom) { gPos.z++; @@ -161,7 +211,9 @@ function setMapStyle() { var mapSel = document.getElementById("mapSelector"); if (mapSel.selectedIndex >= 0 && gActiveMap != mapSel.value) { gActiveMap = mapSel.value; - gTiles = {}; + document.getElementById("copyright").innerHTML = + gMapStyles[gActiveMap].copyright; + showUI(); drawMap(); } } @@ -172,60 +224,56 @@ function mod(a, b) { return ((a % b) + b) % b; } -function normaliseIndices(x, y, 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); - return gMapStyles[gActiveMap].url.replace("{x}", norm.x) - .replace("{y}", norm.y) - .replace("{z}", norm.z); +function normalizeCoords(aCoords) { + var zoomFactor = Math.pow(2, aCoords.z); + return {x: mod(aCoords.x, zoomFactor), + y: mod(aCoords.y, zoomFactor), + z: aCoords.z}; } // Returns true if the tile is outside the current view. function isOutsideWindow(t) { var pos = decodeIndex(t); - var x = pos[0]; - var y = pos[1]; - var z = pos[2]; - var zoomFactor = Math.pow(2, gMaxZoom - z); - var wid = gCanvas.width * zoomFactor; - var ht = gCanvas.height * zoomFactor; + var zoomFactor = Math.pow(2, gMaxZoom - pos.z); + var wid = gMapCanvas.width * zoomFactor; + var ht = gMapCanvas.height * zoomFactor; - x *= zoomFactor; - y *= zoomFactor; + pos.x *= zoomFactor; + pos.y *= zoomFactor; 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) + if (pos.x > gPos.x + wid / 2 || pos.y > gPos.y + ht / 2 || + pos.x + sz < gPos.x - wid / 2 || pos.y - sz < gPos.y - ht / 2) return true; return false; } function encodeIndex(x, y, z) { - var norm = normaliseIndices(x, y, z); + var norm = normalizeCoords({x: x, y: y, z: z}); return norm.x + "," + norm.y + "," + norm.z; } function decodeIndex(encodedIdx) { - return encodedIdx.split(",", 3); + var ind = encodedIdx.split(",", 3); + return {x: ind[0], y: ind[1], z: ind[2]}; } -function drawMap() { - // Go through all the currently loaded tiles. If we don't want any of them remove them. - // for (t in gTiles) { - // if (isOutsideWindow(t)) - // delete gTiles[t]; - // } +function drawMap(aPixels, aOverdraw) { + // aPixels is an object with left/right/top/bottom members telling how many + // pixels on the borders should actually be drawn. + // aOverdraw is a bool that tells if we should draw placeholders or draw + // straight over the existing content. + if (!aPixels) + aPixels = {left: gMapCanvas.width, right: gMapCanvas.width, + top: gMapCanvas.height, bottom: gMapCanvas.height}; + if (!aOverdraw) + aOverdraw = false; + 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 wid = gMapCanvas.width * gZoomFactor; // Width in level 18 pixels. + var ht = gMapCanvas.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. @@ -233,58 +281,135 @@ function drawMap() { 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. + if (gMapPrefsLoaded && mainDB) + gPrefs.set("position", gPos); + + var tiles = {left: Math.ceil((xMin + aPixels.left * gZoomFactor) / size) - + (aPixels.left ? 0 : 1), + right: Math.floor((xMax - aPixels.right * gZoomFactor) / size) - + (aPixels.right ? 1 : 0), + top: Math.ceil((yMin + aPixels.top * gZoomFactor) / size) - + (aPixels.top ? 0 : 1), + bottom: Math.floor((yMax - aPixels.bottom * gZoomFactor) / size) - + (aPixels.bottom ? 1 : 0)}; + + // Go through all the tiles in the map, find out if to draw them and 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). - gContext.drawImage(gTiles[tileKey], Math.round(xoff), Math.round(yoff)); - } - else { - if (!gTiles[tileKey]) { - gTiles[tileKey] = new Image(); - gTiles[tileKey].src = tileURL(x, y, gPos.z); - gTiles[tileKey].onload = function() { - // TODO: Just render this tile where it should be. - // context.drawImage(gTiles[tileKey], Math.round(xoff), Math.round(yoff)); // Doesn't work for some reason. - drawMap(); + for (var y = Math.floor(yMin / size); y < Math.ceil(yMax / size); y++) { // slow script warnings on the tablet appear here! + // Only go to the drawing step if we need to draw this tile. + if (x < tiles.left || x > tiles.right || y < tiles.top || y > tiles.bottom) { + // Round here is **CRUCIAL** otherwise the images are filtered + // and the performance sucks (more than expected). + var xoff = Math.round((x * size - xMin) / gZoomFactor); + var yoff = Math.round((y * size - yMin) / gZoomFactor); + // Draw placeholder tile unless we overdraw. + if (!aOverdraw) + gMapContext.drawImage(gLoadingTile, xoff, yoff); + + // Initiate loading/drawing of the actual tile. + gTileService.get(gActiveMap, {x: x, y: y, z: gPos.z}, + function(aImage, aStyle, aCoords) { + // Only draw if this applies for the current view. + if ((aStyle == gActiveMap) && (aCoords.z == gPos.z)) { + var ixMin = gPos.x - wid / 2; + var iyMin = gPos.y - ht / 2; + var ixoff = Math.round((aCoords.x * size - ixMin) / gZoomFactor); + var iyoff = Math.round((aCoords.y * size - iyMin) / gZoomFactor); + var URL = window.URL; + var imgURL = URL.createObjectURL(aImage); + var imgObj = new Image(); + imgObj.src = imgURL; + imgObj.onload = function() { + gMapContext.drawImage(imgObj, ixoff, iyoff); + URL.revokeObjectURL(imgURL); + } } - } - gContext.drawImage(gLoadingTile, Math.round(xoff), Math.round(yoff)); + }); } } } - if (gTrack.length) + gLastDrawnPoint = null; + gCurPosMapCache = undefined; + gTrackContext.clearRect(0, 0, gTrackCanvas.width, gTrackCanvas.height); + if (gTrack.length) { for (var i = 0; i < gTrack.length; i++) { - drawTrackPoint(gTrack[i].coords.latitude, gTrack[i].coords.longitude); + drawTrackPoint(gTrack[i].coords.latitude, gTrack[i].coords.longitude, + (i + 1 >= gTrack.length)); } + } } -function drawTrackPoint(aLatitude, aLongitude) { +function drawTrackPoint(aLatitude, aLongitude, lastPoint) { 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(); + // lastPoint is for optimizing (not actually executing the draw until the last) + trackpoint.optimized = (lastPoint === false); + var mappos = {x: Math.round((trackpoint.x - gPos.x) / gZoomFactor + gMapCanvas.width / 2), + y: Math.round((trackpoint.y - gPos.y) / gZoomFactor + gMapCanvas.height / 2)}; + + if (!gLastDrawnPoint || !gLastDrawnPoint.optimized) { + gTrackContext.strokeStyle = gTrackColor; + gTrackContext.fillStyle = gTrackContext.strokeStyle; + gTrackContext.lineWidth = gTrackWidth; + gTrackContext.lineCap = "round"; + gTrackContext.lineJoin = "round"; + } + if (!gLastDrawnPoint || gLastDrawnPoint == trackpoint) { + // This breaks optimiziation, so make sure to close path and reset optimization. + if (gLastDrawnPoint && gLastDrawnPoint.optimized) + gTrackContext.stroke(); + gTrackContext.beginPath(); + trackpoint.optimized = false; + gTrackContext.arc(mappos.x, mappos.y, + gTrackContext.lineWidth, 0, Math.PI * 2, false); + gTrackContext.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(); + if (!gLastDrawnPoint || !gLastDrawnPoint.optimized) { + gTrackContext.beginPath(); + gTrackContext.moveTo(Math.round((gLastDrawnPoint.x - gPos.x) / gZoomFactor + gMapCanvas.width / 2), + Math.round((gLastDrawnPoint.y - gPos.y) / gZoomFactor + gMapCanvas.height / 2)); + } + gTrackContext.lineTo(mappos.x, mappos.y); + if (!trackpoint.optimized) + gTrackContext.stroke(); + } + gLastDrawnPoint = trackpoint; +} + +function drawCurrentLocation(trackPoint) { + var locpoint = gps2xy(trackPoint.coords.latitude, trackPoint.coords.longitude); + var circleRadius = Math.round(gCurLocSize / 2); + var mappos = {x: Math.round((locpoint.x - gPos.x) / gZoomFactor + gMapCanvas.width / 2), + y: Math.round((locpoint.y - gPos.y) / gZoomFactor + gMapCanvas.height / 2)}; + + undrawCurrentLocation(); + + // Cache overdrawn area. + gCurPosMapCache = + {point: locpoint, + radius: circleRadius, + data: gTrackContext.getImageData(mappos.x - circleRadius, + mappos.y - circleRadius, + circleRadius * 2, circleRadius * 2)}; + + gTrackContext.strokeStyle = gCurLocColor; + gTrackContext.fillStyle = gTrackContext.strokeStyle; + gTrackContext.beginPath(); + gTrackContext.arc(mappos.x, mappos.y, + circleRadius, 0, Math.PI * 2, false); + gTrackContext.fill(); +} + +function undrawCurrentLocation() { + if (gCurPosMapCache) { + var oldpoint = gCurPosMapCache.point; + var oldmp = {x: Math.round((oldpoint.x - gPos.x) / gZoomFactor + gMapCanvas.width / 2), + y: Math.round((oldpoint.y - gPos.y) / gZoomFactor + gMapCanvas.height / 2)}; + gTrackContext.putImageData(gCurPosMapCache.data, + oldmp.x - gCurPosMapCache.radius, + oldmp.y - gCurPosMapCache.radius); + gCurPosMapCache = undefined; } - gLastTrackPoint = trackpoint; } var mapEvHandler = { @@ -298,41 +423,49 @@ var mapEvHandler = { 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; + var x = coordObj.clientX - gMapCanvas.offsetLeft; + var y = coordObj.clientY - gMapCanvas.offsetTop; if (touchEvent || aEvent.button === 0) { gDragging = true; } gLastMouseX = x; gLastMouseY = y; + showUI(); break; case "mousemove": case "touchmove": - var x = coordObj.clientX - gCanvas.offsetLeft; - var y = coordObj.clientY - gCanvas.offsetTop; + var x = coordObj.clientX - gMapCanvas.offsetLeft; + var y = coordObj.clientY - gMapCanvas.offsetTop; if (gDragging === true) { var dX = x - gLastMouseX; var dY = y - gLastMouseY; gPos.x -= dX * gZoomFactor; gPos.y -= dY * gZoomFactor; - drawMap(); + var mapData = gMapContext.getImageData(0, 0, gMapCanvas.width, gMapCanvas.height); + gMapContext.clearRect(0, 0, gMapCanvas.width, gMapCanvas.height); + gMapContext.putImageData(mapData, dX, dY); + drawMap({left: (dX > 0) ? dX : 0, + right: (dX < 0) ? -dX : 0, + top: (dY > 0) ? dY : 0, + bottom: (dY < 0) ? -dY : 0}); + showUI(); } gLastMouseX = x; gLastMouseY = y; @@ -340,6 +473,7 @@ var mapEvHandler = { case "mouseup": case "touchend": gDragging = false; + showUI(); break; case "mouseout": case "touchcancel": @@ -358,44 +492,60 @@ 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 ptCoord = {x: gPos.x + (x - gMapCanvas.width / 2) * gZoomFactor, + y: gPos.y + (x - gMapCanvas.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; + console.log(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) - zoomOut(); + + var newZoomLevel = gPos.z + (delta > 0 ? 1 : -1); + if ((newZoomLevel >= 0) && (newZoomLevel <= gMaxZoom)) { + // 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 - gMapCanvas.offsetLeft; + var y = coordObj.clientY - gMapCanvas.offsetTop; + + // Zoom factor after this action. + var newZoomFactor = Math.pow(2, gMaxZoom - newZoomLevel); + gPos.x -= (x - gMapCanvas.width / 2) * (newZoomFactor - gZoomFactor); + gPos.y -= (y - gMapCanvas.height / 2) * (newZoomFactor - gZoomFactor); + + if (delta > 0) + zoomIn(); + else if (delta < 0) + zoomOut(); + } break; } } }; -geofake = { +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: 48.208174, // + Math.random() - .5, - longitude: 16.373819, // + Math.random() - .5, + coords: {latitude: geofake.lastPos.x, + longitude: geofake.lastPos.y, accuracy: 20}}); if (geofake.tracking) setTimeout(watchCall, 1000); @@ -408,28 +558,63 @@ geofake = { } } +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() { - if (navigator.geolocation) { - //gGeoWatchID = geofake.watchPosition( - gGeoWatchID = navigator.geolocation.watchPosition( + if (gGeolocation) { + gGeoWatchID = gGeolocation.watchPosition( function(position) { // Coords spec: https://developer.mozilla.org/en/XPCOM_Interface_Reference/NsIDOMGeoPositionCoords - gTrack.push({time: position.timestamp, coords: position.coords}); - 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(); + var tPoint = {time: position.timestamp, + coords: {latitude: position.coords.latitude, + longitude: position.coords.longitude, + altitude: position.coords.altitude, + accuracy: position.coords.accuracy, + altitudeAccuracy: position.coords.altitudeAccuracy, + heading: position.coords.heading, + speed: position.coords.speed}, + beginSegment: !gLastTrackPoint}; + // Only add point to track is accuracy is good enough. + if (tPoint.coords.accuracy < gMinTrackAccuracy) { + gLastTrackPoint = tPoint; + gTrack.push(tPoint); + try { gTrackStore.push(tPoint); } catch(e) {} + var redrawn = false; + if (gCenterPosition) { + var posCoord = gps2xy(position.coords.latitude, + position.coords.longitude); + if (Math.abs(gPos.x - posCoord.x) > gMapCanvas.width * gZoomFactor / 4 || + Math.abs(gPos.y - posCoord.y) > gMapCanvas.height * gZoomFactor / 4) { + gPos.x = posCoord.x; + gPos.y = posCoord.y; + drawMap(); // This draws the current point as well. + redrawn = true; + } } + if (!redrawn) + undrawCurrentLocation(); + drawTrackPoint(position.coords.latitude, position.coords.longitude, true); } + drawCurrentLocation(tPoint); }, 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; + if (gDebug) + console.log(error.message); }, {enableHighAccuracy: true} ); @@ -438,6 +623,100 @@ function startTracking() { function endTracking() { if (gGeoWatchID) { - navigator.geolocation.clearWatch(gGeoWatchID); + gGeolocation.clearWatch(gGeoWatchID); } } + +function clearTrack() { + gTrack = []; + gTrackStore.clear(); + drawMap(); +} + +var gTileService = { + objStore: "tilecache", + + get: function(aStyle, aCoords, aCallback) { + var norm = normalizeCoords(aCoords); + var dbkey = aStyle + "::" + norm.x + "," + norm.y + "," + norm.z; + this.getDBCache(dbkey, function(aResult, aEvent) { + if (aResult) { + // We did get a cached object. + // TODO: Look at the timestamp and trigger a reload when it's too old. + aCallback(aResult.image, aStyle, aCoords); + } + else { + // Retrieve image from the web and store it in the cache. + var XHR = new XMLHttpRequest(); + XHR.open("GET", + gMapStyles[aStyle].url + .replace("{x}", norm.x) + .replace("{y}", norm.y) + .replace("{z}", norm.z) + .replace("[a-c]", String.fromCharCode(97 + Math.floor(Math.random() * 2))) + .replace("[1-4]", 1 + Math.floor(Math.random() * 3)), + true); + XHR.responseType = "blob"; + XHR.addEventListener("load", function () { + if (XHR.status === 200) { + var blob = XHR.response; + gTileService.setDBCache(dbkey, {image: blob, timestamp: Date.now()}); + aCallback(blob, aStyle, aCoords); + } + }, false); + XHR.send(); + } + }); + }, + + getDBCache: function(aKey, aCallback) { + if (!mainDB) + return; + var transaction = mainDB.transaction([this.objStore]); + var request = transaction.objectStore(this.objStore).get(aKey); + request.onsuccess = function(event) { + aCallback(request.result, event); + }; + request.onerror = function(event) { + // Errors can be handled here. + aCallback(undefined, event); + }; + }, + + setDBCache: function(aKey, aValue, aCallback) { + if (!mainDB) + return; + var success = false; + var transaction = mainDB.transaction([this.objStore], "readwrite"); + var objStore = transaction.objectStore(this.objStore); + var request = objStore.put(aValue, aKey); + request.onsuccess = function(event) { + success = true; + if (aCallback) + aCallback(success, event); + }; + request.onerror = function(event) { + // Errors can be handled here. + if (aCallback) + aCallback(success, event); + }; + }, + + unsetDBCache: function(aKey, aCallback) { + if (!mainDB) + return; + var success = false; + var transaction = mainDB.transaction([this.objStore], "readwrite"); + var request = transaction.objectStore(this.objStore).delete(aKey); + request.onsuccess = function(event) { + success = true; + if (aCallback) + aCallback(success, event); + }; + request.onerror = function(event) { + // Errors can be handled here. + if (aCallback) + aCallback(success, event); + } + } +};