diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog index 2a6c15a09..4b8dd108c 100644 --- a/apps/openstmap/ChangeLog +++ b/apps/openstmap/ChangeLog @@ -12,3 +12,5 @@ Fix alignment of satellite info text 0.12: switch to using normal OpenStreetMap tiles (opentopomap was too slow) 0.13: Use a single image file with 'frames' of data (drastically reduces file count, possibility of >1 map on device) +0.14: Added ability to upload multiple sets of map tiles + Support for zooming in on map diff --git a/apps/openstmap/README.md b/apps/openstmap/README.md new file mode 100644 index 000000000..707dbc7f8 --- /dev/null +++ b/apps/openstmap/README.md @@ -0,0 +1,48 @@ +# OpenStreetMap + +This app allows you to upload and use OpenSteetMap map tiles onto your +Bangle. There's an uploader, the app, and also a library that +allows you to use the maps in your Bangle.js applications. + +## Uploader + +Once you've installed OpenStreepMap on your Bangle, find it +in the App Loader and click the Disk icon next to it. + +A window will pop up showing what maps you have loaded. + +To add a map: + +* Click `Add Map` +* Scroll and zoom to the area of interest or use the Search button in the top left +* Now choose the size you want to upload (Small/Medium/etc) +* On Bangle.js 1 you can choose if you want a 3 bits per pixel map (this is lower +quality but uploads faster and takes less space). On Bangle.js 2 you only have a 3bpp +display so can only use 3bpp. +* Click `Get Map`, and a preview will be displayed. If you need to adjust the area you +can change settings, move the map around, and click `Get Map` again. +* When you're ready, click `Upload` + +## Bangle.js App + +The Bangle.js app allows you to view a map - it also turns the GPS on and marks +the path that you've been travelling. + +* Drag on the screen to move the map +* Press the button to bring up a menu, where you can zoom, go to GPS location +or put the map back in its default location + +## Library + +See the documentation in the library itself for full usage info: +https://github.com/espruino/BangleApps/blob/master/apps/openstmap/openstmap.js + +Or check the app itself: https://github.com/espruino/BangleApps/blob/master/apps/openstmap/app.js + +But in the most simple form: + +``` +var m = require("openstmap"); +// m.lat/lon are now the center of the loaded map +m.draw(); // draw centered on the middle of the loaded map +``` diff --git a/apps/openstmap/app.js b/apps/openstmap/app.js index 62597ca20..99362d97b 100644 --- a/apps/openstmap/app.js +++ b/apps/openstmap/app.js @@ -1,20 +1,27 @@ var m = require("openstmap"); var HASWIDGETS = true; -var y1,y2; +var R; var fix = {}; +var mapVisible = false; +var hasScrolled = false; +// Redraw the whole page function redraw() { - g.setClipRect(0,y1,g.getWidth()-1,y2); + g.setClipRect(R.x,R.y,R.x2,R.y2); m.draw(); drawMarker(); - if (WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) { - g.flip(); // force immediate draw on double-buffered screens - track will update later - g.setColor(0.75,0.2,0); + if (HASWIDGETS && WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) { + g.flip().setColor("#f00"); // force immediate draw on double-buffered screens - track will update later WIDGETS["gpsrec"].plotTrack(m); } + if (HASWIDGETS && WIDGETS["recorder"] && WIDGETS["recorder"].plotTrack) { + g.flip().setColor("#f00"); // force immediate draw on double-buffered screens - track will update later + WIDGETS["recorder"].plotTrack(m); + } g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); } +// Draw the marker for where we are function drawMarker() { if (!fix.fix) return; var p = m.latLonToXY(fix.lat, fix.lon); @@ -22,50 +29,66 @@ function drawMarker() { g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2); } -var fix; Bangle.on('GPS',function(f) { fix=f; - g.reset().clearRect(0,y1,g.getWidth()-1,y1+8).setFont("6x8").setFontAlign(0,0); - var txt = fix.satellites+" satellites"; - if (!fix.fix) - txt += " - NO FIX"; - g.drawString(txt,g.getWidth()/2,y1 + 4); - drawMarker(); + if (HASWIDGETS) WIDGETS["sats"].draw(WIDGETS["sats"]); + if (mapVisible) drawMarker(); }); Bangle.setGPSPower(1, "app"); if (HASWIDGETS) { Bangle.loadWidgets(); + WIDGETS["sats"] = { area:"tl", width:48, draw:w=>{ + var txt = (0|fix.satellites)+" Sats"; + if (!fix.fix) txt += "\nNO FIX"; + g.reset().setFont("6x8").setFontAlign(0,0) + .drawString(txt,w.x+24,w.y+12); + } + }; Bangle.drawWidgets(); - y1 = 24; - var hasBottomRow = Object.keys(WIDGETS).some(w=>WIDGETS[w].area[0]=="b"); - y2 = g.getHeight() - (hasBottomRow ? 24 : 1); -} else { - y1=0; - y2=g.getHeight()-1; } +R = Bangle.appRect; -redraw(); - -function recenter() { - if (!fix.fix) return; - m.lat = fix.lat; - m.lon = fix.lon; +function showMap() { + mapVisible = true; + g.reset().clearRect(R); redraw(); + Bangle.setUI({mode:"custom",drag:e=>{ + if (e.b) { + g.setClipRect(R.x,R.y,R.x2,R.y2); + g.scroll(e.dx,e.dy); + m.scroll(e.dx,e.dy); + g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); + hasScrolled = true; + } else if (hasScrolled) { + hasScrolled = false; + redraw(); + } + }, btn: btn=>{ + mapVisible = false; + var menu = {"":{title:"Map"}, + "< Back": ()=> showMap(), + /*LANG*/"Zoom In": () =>{ + m.scale /= 2; + showMap(); + }, + /*LANG*/"Zoom Out": () =>{ + m.scale *= 2; + showMap(); + }, + /*LANG*/"Center Map": () =>{ + m.lat = m.map.lat; + m.lon = m.map.lon; + m.scale = m.map.scale; + showMap(); + }}; + if (fix.fix) menu[/*LANG*/"Center GPS"]=() =>{ + m.lat = fix.lat; + m.lon = fix.lon; + showMap(); + }; + E.showMenu(menu); + }}); } -setWatch(recenter, global.BTN2?BTN2:BTN1, {repeat:true}); - -var hasScrolled = false; -Bangle.on('drag',e=>{ - if (e.b) { - g.setClipRect(0,y1,g.getWidth()-1,y2); - g.scroll(e.dx,e.dy); - m.scroll(e.dx,e.dy); - g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); - hasScrolled = true; - } else if (hasScrolled) { - hasScrolled = false; - redraw(); - } -}); +showMap(); diff --git a/apps/openstmap/custom.html b/apps/openstmap/interface.html similarity index 62% rename from apps/openstmap/custom.html rename to apps/openstmap/interface.html index c1a161458..81406d807 100644 --- a/apps/openstmap/custom.html +++ b/apps/openstmap/interface.html @@ -9,7 +9,8 @@ padding: 0; margin: 0; } - html, body, #map { + html, body, #map, #mapsLoaded, #mapContainer { + position: relative; height: 100%; width: 100%; } @@ -27,20 +28,40 @@ width: 256px; height: 256px; } + .tile-title { + font-weight:bold; + font-size: 125%; + } + .tile-map { + width: 128px; + height: 128px; + } -
+
-
-

3 bit
-
- - +
+
+
+
+

3 bit
+
+ +
+
+ + +
- + @@ -60,8 +81,6 @@ TODO: */ var TILESIZE = 96; // Size of our tiles var OSMTILESIZE = 256; // Size of openstreetmap tiles - var MAPSIZE = TILESIZE*5; ///< 480 - Size of map we download - var OSMTILECOUNT = 3; // how many tiles do we download in each direction (Math.floor(MAPSIZE / OSMTILESIZE)+1) /* Can see possible tiles on http://leaflet-extras.github.io/leaflet-providers/preview/ However some don't allow cross-origin use */ //var TILELAYER = 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'; // simple, high contrast, TOO SLOW @@ -69,8 +88,8 @@ TODO: var TILELAYER = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; var PREVIEWTILELAYER = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; - // Create map and try and set the location to where the browser thinks we are - var map = L.map('map').locate({setView: true, maxZoom: 16, enableHighAccuracy:true}); + var loadedMaps = []; + // Tiles used for Bangle.js itself var bangleTileLayer = L.tileLayer(TILELAYER, { maxZoom: 18, @@ -83,6 +102,10 @@ TODO: }); // Could optionally overlay trails: https://wiki.openstreetmap.org/wiki/Tiles + // Create map and try and set the location to where the browser thinks we are + var map = L.map('map').locate({setView: true, maxZoom: 16, enableHighAccuracy:true}); + previewTileLayer.addTo(map); + // Search box: const searchProvider = new window.GeoSearch.OpenStreetMapProvider(); const searchControl = new GeoSearch.GeoSearchControl({ @@ -96,6 +119,7 @@ TODO: }); map.addControl(searchControl); + // ---------------------------------------- Run at startup function onInit(device) { if (device && device.info && device.info.g) { // On 3 bit devices, don't even offer the option. 3 bit is the only way @@ -104,12 +128,120 @@ TODO: document.getElementById("3bitdiv").style = "display:none"; } } + + showLoadedMaps(); } - var mapFiles = []; - previewTileLayer.addTo(map); + function showLoadedMaps() { + document.getElementById("mapsLoadedContainer").style.display = ""; + document.getElementById("mapContainer").style.display = "none"; - function tilesLoaded(ctx, width, height) { + Util.showModal("Loading maps..."); + let mapsLoadedContainer = document.getElementById("mapsLoadedContainer"); + mapsLoadedContainer.innerHTML = ""; + loadedMaps = []; + + Puck.write(`\x10Bluetooth.println(require("Storage").list(/openstmap\\.\\d+\\.json/))\n`,function(files) { + console.log("MAPS:",files); + let promise = Promise.resolve(); + files.trim().split(",").forEach(filename => { + if (filename=="") return; + promise = promise.then(() => new Promise(resolve => { + Util.readStorage(filename, fileContents => { + console.log(filename + " => " + fileContents); + let mapNumber = filename.match(/\d+/)[0]; // figure out what map number we are + let mapInfo; + try { + mapInfo = JSON.parse(fileContents); + } catch (e) { + console.error(e); + } + loadedMaps[mapNumber] = mapInfo; + if (mapInfo!==undefined) { + let latlon = L.latLng(mapInfo.lat, mapInfo.lon); + mapsLoadedContainer.innerHTML += ` +
+
+
+
+
+
+

Map ${mapNumber}

+

${mapInfo.w*mapInfo.h} Tiles (${((mapInfo.imgx*mapInfo.imgy)>>11).toFixed(0)}k)

+
+
+ +
+
+ `; + let map = L.map(`tile-map-${mapNumber}`); + L.tileLayer(PREVIEWTILELAYER, { + maxZoom: 18 + }).addTo(map); + let marker = new L.marker(latlon).addTo(map); + map.fitBounds(latlon.toBounds(2000/*meters*/), {animation: false}); + } + resolve(); + }); + })); + }); + promise = promise.then(() => new Promise(resolve => { + if (!loadedMaps.length) { + mapsLoadedContainer.innerHTML += ` +
+
+
+
+
+
+

No Maps Loaded

+
+
+
+
+ `; + } + mapsLoadedContainer.innerHTML += ` +
+
+
+
+
+
+
+
+ +
+
+ `; + Util.hideModal(); + })); + }); + } + + function onMapDelete(mapNumber) { + console.log("delete", mapNumber); + Util.showModal(`Erasing map ${mapNumber}...`); + Util.eraseStorage(`openstmap.${mapNumber}.json`, function() { + Util.eraseStorage(`openstmap.${mapNumber}.img`, function() { + Util.hideModal(); + showLoadedMaps(); + }); + }); + } + + function showMap() { + document.getElementById("mapsLoadedContainer").style.display = "none"; + document.getElementById("mapContainer").style.display = ""; + document.getElementById("maptiles").style.display="none"; + document.getElementById("uploadbuttons").style.display="none"; + } + + // ----------------------------------------------------- + var mapFiles = []; + + // convert canvas into an actual tiled image file + function tilesLoaded(ctx, width, height, mapImageFile) { var options = { compression:false, output:"raw", mode:"web" @@ -166,12 +298,17 @@ TODO: } } return [{ - name:"openstmap.0.img", + name:mapImageFile, content:tiledImage }]; } document.getElementById("getmap").addEventListener("click", function() { + + var MAPTILES = parseInt(document.getElementById("mapSize").value); + var MAPSIZE = TILESIZE*MAPTILES; /// Size of map we download to Bangle in pixels + var OSMTILECOUNT = (Math.ceil((MAPSIZE+TILESIZE) / OSMTILESIZE)+1); // how many tiles do we download from OSM in each direction + var zoom = map.getZoom(); var centerlatlon = map.getBounds().getCenter(); var center = map.project(centerlatlon, zoom).divideBy(OSMTILESIZE); @@ -242,8 +379,11 @@ TODO: Promise.all(tileGetters).then(() => { document.getElementById("uploadbuttons").style.display=""; - mapFiles = tilesLoaded(ctx, canvas.width, canvas.height); - mapFiles.unshift({name:"openstmap.0.json",content:JSON.stringify({ + var mapNumber = 0; + while (loadedMaps[mapNumber]) mapNumber++; + let mapImageFile = `openstmap.${mapNumber}.img`; + mapFiles = tilesLoaded(ctx, canvas.width, canvas.height, mapImageFile); + mapFiles.unshift({name:`openstmap.${mapNumber}.json`,content:JSON.stringify({ imgx : canvas.width, imgy : canvas.height, tilesize : TILESIZE, @@ -252,21 +392,31 @@ TODO: lon : centerlatlon.lng, w : Math.round(canvas.width / TILESIZE), // width in tiles h : Math.round(canvas.height / TILESIZE), // height in tiles - fn : "openstmap.0.img" + fn : mapImageFile })}); console.log(mapFiles); }); }); document.getElementById("upload").addEventListener("click", function() { - sendCustomizedApp({ - storage:mapFiles + Util.showModal("Uploading..."); + let promise = Promise.resolve(); + mapFiles.forEach(file => { + promise = promise.then(function() { + return new Promise(resolve => { + Util.writeStorage(file.name, file.content, resolve); + }); + }); + }); + promise.then(function() { + Util.hideModal(); + console.log("Upload Complete"); + showLoadedMaps(); }); }); document.getElementById("cancel").addEventListener("click", function() { - document.getElementById("maptiles").style.display="none"; - document.getElementById("uploadbuttons").style.display="none"; + showMap(); }); diff --git a/apps/openstmap/metadata.json b/apps/openstmap/metadata.json index 32093f70e..09b4c68e3 100644 --- a/apps/openstmap/metadata.json +++ b/apps/openstmap/metadata.json @@ -2,17 +2,20 @@ "id": "openstmap", "name": "OpenStreetMap", "shortName": "OpenStMap", - "version": "0.13", + "version": "0.14", "description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps", + "readme": "README.md", "icon": "app.png", "tags": "outdoors,gps,osm", "supports": ["BANGLEJS","BANGLEJS2"], "screenshots": [{"url":"screenshot.png"}], - "custom": "custom.html", - "customConnect": true, + "interface": "interface.html", "storage": [ {"name":"openstmap","url":"openstmap.js"}, {"name":"openstmap.app.js","url":"app.js"}, {"name":"openstmap.img","url":"app-icon.js","evaluate":true} + ], "data": [ + {"wildcard":"openstmap.*.json"}, + {"wildcard":"openstmap.*.img"} ] } diff --git a/apps/openstmap/openstmap.js b/apps/openstmap/openstmap.js index 2bd7d2e2e..692344357 100644 --- a/apps/openstmap/openstmap.js +++ b/apps/openstmap/openstmap.js @@ -20,32 +20,59 @@ function center() { m.draw(); } +// you can even change the scale - eg 'm/scale *= 2' + */ -var map = require("Storage").readJSON("openstmap.0.json"); -map.center = Bangle.project({lat:map.lat,lon:map.lon}); -exports.map = map; -exports.lat = map.lat; // actual position of middle of screen -exports.lon = map.lon; // actual position of middle of screen var m = exports; +m.maps = require("Storage").list(/openstmap\.\d+\.json/).map(f=>{ + let map = require("Storage").readJSON(f); + map.center = Bangle.project({lat:map.lat,lon:map.lon}); + return map; +}); +// we base our start position on the middle of the first map +m.map = m.maps[0]; +m.scale = m.map.scale; // current scale (based on first map) +m.lat = m.map.lat; // position of middle of screen +m.lon = m.map.lon; // position of middle of screen exports.draw = function() { - var img = require("Storage").read(map.fn); var cx = g.getWidth()/2; var cy = g.getHeight()/2; var p = Bangle.project({lat:m.lat,lon:m.lon}); - var ix = (p.x-map.center.x)/map.scale + (map.imgx/2) - cx; - var iy = (map.center.y-p.y)/map.scale + (map.imgy/2) - cy; - //console.log(ix,iy); - var tx = 0|(ix/map.tilesize); - var ty = 0|(iy/map.tilesize); - var ox = (tx*map.tilesize)-ix; - var oy = (ty*map.tilesize)-iy; - for (var x=ox,ttx=tx;x=0 && ttx=0 && tty { + var d = map.scale/m.scale; + var ix = (p.x-map.center.x)/m.scale + (map.imgx*d/2) - cx; + var iy = (map.center.y-p.y)/m.scale + (map.imgy*d/2) - cy; + var o = {}; + var s = map.tilesize; + if (d!=1) { // if the two are different, add scaling + s *= d; + o.scale = d; } + //console.log(ix,iy); + var tx = 0|(ix/s); + var ty = 0|(iy/s); + var ox = (tx*s)-ix; + var oy = (ty*s)-iy; + var img = require("Storage").read(map.fn); + // fix out of range so we don't have to iterate over them + if (tx<0) { + ox+=s*-tx; + tx=0; + } + if (ty<0) { + oy+=s*-ty; + ty=0; + } + var mx = g.getWidth(); + var my = g.getHeight(); + for (var x=ox,ttx=tx; x