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
master
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);