openstmap

Added ability to upload multiple sets of map tiles
      Support for zooming in on map
      Satellite count moved to widget bar to leave more room for the map
pull/2297/head
Gordon Williams 2022-11-25 15:34:51 +00:00
parent f4a48e62e6
commit 459a643c87
7 changed files with 343 additions and 89 deletions

View File

@ -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

48
apps/openstmap/README.md Normal file
View File

@ -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
```

View File

@ -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();

View File

@ -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;
}
</style>
</head>
<body>
<div id="map">
<div id="mapsLoadedContainer">
</div>
<div id="controls">
<div style="display:inline-block;text-align:center;vertical-align: top;" id="3bitdiv"> <input type="checkbox" id="3bit"></input><br/><span>3 bit</span></div>
<button id="getmap" class="btn btn-primary">Get Map</button><br/>
<canvas id="maptiles" style="display:none"></canvas>
<div id="uploadbuttons" style="display:none"><button id="upload" class="btn btn-primary">Upload</button>
<button id="cancel" class="btn">Cancel</button></div>
<div id="mapContainer">
<div id="map">
</div>
<div id="controls">
<div style="display:inline-block;text-align:center;vertical-align: top;" id="3bitdiv"> <input type="checkbox" id="3bit"></input><br/><span>3 bit</span></div>
<div class="form-group" style="display:inline-block;">
<select class="form-select" id="mapSize">
<option value="4">Small</option>
<option value="5" selected>Medium</option>
<option value="6">Large</option>
<option value="7">XL</option>
</select>
</div>
<button id="getmap" class="btn btn-primary">Get Map</button><button class="btn" onclick="showLoadedMaps()">Map List</button><br/>
<canvas id="maptiles" style="display:none"></canvas>
<div id="uploadbuttons" style="display:none"><button id="upload" class="btn btn-primary">Upload</button>
<button id="cancel" class="btn">Cancel</button></div>
</div>
</div>
<script src="../../core/lib/customize.js"></script>
<script src="../../core/lib/interface.js"></script>
<script src="https://unpkg.com/leaflet@1.0.3/dist/leaflet.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="../../webtools/heatshrink.js"></script>
@ -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 += `
<div class="tile">
<div class="tile-icon">
<div class="tile-map" id="tile-map-${mapNumber}">
</div>
</div>
<div class="tile-content">
<p class="tile-title">Map ${mapNumber}</p>
<p class="tile-subtitle">${mapInfo.w*mapInfo.h} Tiles (${((mapInfo.imgx*mapInfo.imgy)>>11).toFixed(0)}k)</p>
</div>
<div class="tile-action">
<button class="btn btn-primary" onclick="onMapDelete(${mapNumber})">Delete</button>
</div>
</div>
`;
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 += `
<div class="tile">
<div class="tile-icon">
<div class="tile-map">
</div>
</div>
<div class="tile-content">
<p class="tile-title">No Maps Loaded</p>
</div>
<div class="tile-action">
</div>
</div>
`;
}
mapsLoadedContainer.innerHTML += `
<div class="tile">
<div class="tile-icon">
<div class="tile-map">
</div>
</div>
<div class="tile-content">
</div>
<div class="tile-action">
<button class="btn btn-primary" onclick="showMap()">Add Map</button>
</div>
</div>
`;
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();
});
</script>

View File

@ -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"}
]
}

View File

@ -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<g.getWidth();x+=map.tilesize,ttx++)
for (var y=oy,tty=ty;y<g.getHeight();y+=map.tilesize,tty++) {
if (ttx>=0 && ttx<map.w && tty>=0 && tty<map.h) g.drawImage(img,x,y,{frame:ttx+(tty*map.w)});
else g.clearRect(x,y,x+map.tilesize-1,y+map.tilesize-1).drawLine(x,y,x+map.tilesize-1,y+map.tilesize-1).drawLine(x,y+map.tilesize-1,x+map.tilesize-1,y);
m.maps.forEach((map,idx) => {
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<mx && ttx<map.w; x+=s,ttx++)
for (var y=oy,tty=ty;y<my && tty<map.h;y+=s,tty++) {
o.frame = ttx+(tty*map.w);
g.drawImage(img,x,y,o);
}
});
};
/// Convert lat/lon to pixels on the screen
@ -55,15 +82,15 @@ exports.latLonToXY = function(lat, lon) {
var cx = g.getWidth()/2;
var cy = g.getHeight()/2;
return {
x : (q.x-p.x)/map.scale + cx,
y : cy - (q.y-p.y)/map.scale
x : (q.x-p.x)/m.scale + cx,
y : cy - (q.y-p.y)/m.scale
};
};
/// Given an amount to scroll in pixels on the screen, adjust the lat/lon of the map to match
exports.scroll = function(x,y) {
var a = Bangle.project({lat:this.lat,lon:this.lon});
var b = Bangle.project({lat:this.lat+1,lon:this.lon+1});
this.lon += x * this.map.scale / (a.x-b.x);
this.lat -= y * this.map.scale / (a.y-b.y);
var a = Bangle.project({lat:m.lat,lon:m.lon});
var b = Bangle.project({lat:m.lat+1,lon:m.lon+1});
this.lon += x * m.scale / (a.x-b.x);
this.lat -= y * m.scale / (a.y-b.y);
};

View File

@ -266,6 +266,7 @@
WIDGETS["recorder"].reload();
return Promise.resolve(settings.recording);
}/*,plotTrack:function(m) { // m=instance of openstmap module
// FIXME - add track plotting
// if we're here, settings was already loaded
var f = require("Storage").open(settings.file,"r");
var l = f.readLine(f);