/* original openstmap.js */ /* OpenStreetMap plotting module. Usage: 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 // plot gps position on map Bangle.on('GPS',function(f) { if (!f.fix) return; var p = m.latLonToXY(fix.lat, fix.lon); g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2); }); // recenter and redraw map! function center() { m.lat = fix.lat; m.lon = fix.lon; m.draw(); } // you can even change the scale - eg 'm/scale *= 2' */ var exports = {}; 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 if (m.maps[0] != undefined) { 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 } else { m.scale = 20; m.lat = 50; m.lon = 14; } exports.draw = function() { var cx = g.getWidth()/2; var cy = g.getHeight()/2; var p = Bangle.project({lat:m.lat,lon:m.lon}); 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 ac * expansion) && (x = ac * expansion); (y > ac) && (y = ac); //(x < 0) && (x = 0); //(y < 0) && (y = 0); return [x, y]; } // Convert bbox to xyx bounds // // - `bbox` {Number} bbox in the form `[w, s, e, n]`. // - `zoom` {Number} zoom. // - `tms_style` {Boolean} whether to compute using tms-style. // - `srs` {String} projection of input bbox (WGS84|900913). // - `@return` {Object} XYZ bounds containing minX, maxX, minY, maxY properties. xyz = function(bbox, zoom, tms_style, srs) { // If web mercator provided reproject to WGS84. if (srs === '900913') { bbox = this.convert(bbox, 'WGS84'); } var ll = [bbox[0], bbox[1]]; // lower left var ur = [bbox[2], bbox[3]]; // upper right var px_ll = px(ll, zoom); var px_ur = px(ur, zoom); // Y = 0 for XYZ is the top hence minY uses px_ur[1]. var size = 256; var x = [ Math.floor(px_ll[0] / size), Math.floor((px_ur[0] - 1) / size) ]; var y = [ Math.floor(px_ur[1] / size), Math.floor((px_ll[1] - 1) / size) ]; var bounds = { minX: Math.min.apply(Math, x) < 0 ? 0 : Math.min.apply(Math, x), minY: Math.min.apply(Math, y) < 0 ? 0 : Math.min.apply(Math, y), maxX: Math.max.apply(Math, x), maxY: Math.max.apply(Math, y) }; if (tms_style) { var tms = { minY: (Math.pow(2, zoom) - 1) - bounds.maxY, maxY: (Math.pow(2, zoom) - 1) - bounds.minY }; bounds.minY = tms.minY; bounds.maxY = tms.maxY; } return bounds; }; // Convert screen pixel value to lon lat // // - `px` {Array} `[x, y]` array of geographic coordinates. // - `zoom` {Number} zoom level. function ll(px, zoom) { var size = 256 * Math.pow(2, zoom); var bc = (size / 360); var cc = (size / (2 * Math.PI)); var zc = size / 2; var g = (px[1] - zc) / -cc; var lon = (px[0] - zc) / bc; var R2D = 180 / Math.PI; var lat = R2D * (2 * Math.atan(Math.exp(g)) - 0.5 * Math.PI); return [lon, lat]; } // Convert tile xyz value to bbox of the form `[w, s, e, n]` // // - `x` {Number} x (longitude) number. // - `y` {Number} y (latitude) number. // - `zoom` {Number} zoom. // - `tms_style` {Boolean} whether to compute using tms-style. // - `srs` {String} projection for resulting bbox (WGS84|900913). // - `return` {Array} bbox array of values in form `[w, s, e, n]`. bbox = function(x, y, zoom, tms_style, srs) { var size = 256; // Convert xyz into bbox with srs WGS84 if (tms_style) { y = (Math.pow(2, zoom) - 1) - y; } // Use +y to make sure it's a number to avoid inadvertent concatenation. var ll_ = [x * size, (+y + 1) * size]; // lower left // Use +x to make sure it's a number to avoid inadvertent concatenation. var ur = [(+x + 1) * size, y * size]; // upper right var bbox = ll(ll_, zoom).concat(ll(ur, zoom)); // If web mercator requested reproject to 900913. if (srs === '900913') { return this.convert(bbox, '900913'); } else { return bbox; } }; /* original openstmap_app.js */ //var m = require("openstmap"); var HASWIDGETS = true; var R; var fix = {}; var mapVisible = false; var hasScrolled = false; var settings = require("Storage").readJSON("openstmap.json",1)||{}; var points; var startDrag = 0; // Redraw the whole page function redraw(qual) { if (1) drawAll(qual); g.setClipRect(R.x,R.y,R.x2,R.y2); if (0) m.draw(); drawPOI(); drawMarker(); // if track drawing is enabled... if (settings.drawTrack) { if (HASWIDGETS && WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) { g.setColor("#f00").flip(); // force immediate draw on double-buffered screens - track will update later WIDGETS["gpsrec"].plotTrack(m); } if (HASWIDGETS && WIDGETS["recorder"] && WIDGETS["recorder"].plotTrack) { g.setColor("#f00").flip(); // 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 POIs function drawPOI() { if (1) return; /* var waypoints = require("waypoints").load(); FIXME */ g.setFont("Vector", 18); waypoints.forEach((wp, idx) => { var p = m.latLonToXY(wp.lat, wp.lon); var sz = 2; g.setColor(0,0,0); g.fillRect(p.x-sz, p.y-sz, p.x+sz, p.y+sz); g.setColor(0,0,0); g.drawString(wp.name, p.x, p.y); print(wp.name); }) } // Draw the marker for where we are function drawMarker() { if (!fix.fix) return; var p = m.latLonToXY(fix.lat, fix.lon); g.setColor(1,0,0); g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2); } Bangle.on('GPS',function(f) { fix=f; 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(); } R = Bangle.appRect; function showMap() { mapVisible = true; g.reset().clearRect(R); redraw(0); emptyMap(); } function emptyMap() { Bangle.setUI({mode:"custom",drag:e=>{ if (e.b) { if (!startDrag) startDrag = getTime(); 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; print("Has scrolled"); } else if (hasScrolled) { delta = getTime() - startDrag; startDrag = 0; hasScrolled = false; print("Done", delta, e.x, e.y); qual = 0; if (delta < 0.2) { if (e.x < g.getWidth() / 2) { if (e.y < g.getHeight() / 2) { m.scale /= 2; } else { m.scale *= 2; } } else { if (e.y < g.getHeight() / 2) { qual = 2; } else { qual = 4; } } } g.reset().clearRect(R); redraw(qual); } }, 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*/"Draw Track": { value : !!settings.drawTrack, onchange : v => { settings.drawTrack=v; require("Storage").writeJSON("openstmap.json",settings); } }, /*LANG*/"Center Map": () =>{ m.lat = m.map.lat; m.lon = m.map.lon; m.scale = m.map.scale; showMap(); }, /*LANG*/"Benchmark": () =>{ m.lat = 50.001; m.lon = 14.759; m.scale = 2; g.reset().clearRect(R); redraw(18); print("Benchmark done"); } }; if (fix.fix) menu[/*LANG*/"Center GPS"]=() =>{ m.lat = fix.lat; m.lon = fix.lon; showMap(); }; E.showMenu(menu); }}); } var gjson = null; function stringFromArray(data) { var count = data.length; var str = ""; for(var index = 0; index < count; index += 1) str += String.fromCharCode(data[index]); return str; } const st = require('Storage'); const hs = require('heatshrink'); function readTarFile(tar, f) { let json_off = st.read(tar, 0, 16) * 1; if (isNaN(json_off)) { print("Don't have archive", tar); return undefined; } while (1) { let json_len = st.read(tar, json_off, 6) * 1; if (json_len == -1) break; json_off += 6; let json = st.read(tar, json_off, json_len); //print("Have directory, ", json.length, "bytes"); let files = JSON.parse(json); let rec = files[f]; if (rec) { let cs = st.read(tar, rec.st, rec.si); if (rec.comp == "hs") { let d = stringFromArray(hs.decompress(cs)); //print("Decompressed", d); return d; } return cs; } json_off += json_len; } return undefined; } function loadVector(name) { var t1 = getTime(); print(".. Read", name); //s = require("Storage").read(name); var s = readTarFile("world.mtar", name); if (s == undefined) { print("Don't have file", name); return null; } var r = JSON.parse(s); print(".... Read and parse took ", getTime()-t1); return r; } function drawPoint(a) { /* FIXME: let... */ lon = a.geometry.coordinates[0]; lat = a.geometry.coordinates[1]; var p = m.latLonToXY(lat, lon); var sz = 2; if (a.properties["marker-color"]) { g.setColor(a.properties["marker-color"]); } if (a.properties.marker_size == "small") sz = 1; if (a.properties.marker_size == "large") sz = 4; g.fillRect(p.x-sz, p.y-sz, p.x+sz, p.y+sz); g.setColor(0,0,0); g.setFont("Vector", 18).setFontAlign(-1,-1); g.drawString(a.properties.name, p.x, p.y); points ++; } function drawLine(a, qual) { lon = a.geometry.coordinates[0][0]; lat = a.geometry.coordinates[0][1]; i = 1; step = 1; len = a.geometry.coordinates.length; step = step * qual; var p1 = m.latLonToXY(lat, lon); if (a.properties.stroke) { g.setColor(a.properties.stroke); } while (i < len) { lon = a.geometry.coordinates[i][0]; lat = a.geometry.coordinates[i][1]; var p2 = m.latLonToXY(lat, lon); //print(p1.x, p1.y, p2.x, p2.y); g.drawLine(p1.x, p1.y, p2.x, p2.y); if (i == len-1) break; i = i + step; if (i>len) i = len-1; points ++; p1 = p2; } } function drawPolygon(a, qual) { lon = a.geometry.coordinates[0][0]; lat = a.geometry.coordinates[0][1]; i = 1; step = 1; len = a.geometry.coordinates.length; if (len > 62) { step = log2(len) - 5; step = 1<len) i = len-1; points ++; } if (a.properties.fill) { g.setColor(a.properties.fill); } else { g.setColor(.75, .75, 1); } g.fillPoly(pol, true); if (a.properties.stroke) { g.setColor(a.properties.stroke); } else { g.setColor(0,0,0) } g.drawPoly(pol, true); } function toScreen(tile, xy) { // w, s, e, n, (x,y in 0..4096 range) let x = xy[0]; let y = xy[1]; let r = {}; r.x = ((x/4096) * (tile[2]-tile[0])) + tile[0]; r.y = ((1-(y/4096)) * (tile[3]-tile[1])) + tile[1]; return r; } var d_off = 1; function getBin(bin, i, prev) { let x = bin[i*3 + d_off ]<<4; let y = bin[i*3 + d_off+1]<<4; //print("Point", x, y, bin); return [x, y]; } function getBinLength(bin) { return (bin.length-d_off) / 3; } function newPoint(tile, a, rec, bin) { var p = toScreen(tile, getBin(bin, 0, null)); var sz = 2; if (a.properties) { if (a.properties["marker-color"]) { g.setColor(a.properties["marker-color"]); } if (a.properties.marker_size == "small") sz = 1; if (a.properties.marker_size == "large") sz = 4; } g.fillRect(p.x-sz, p.y-sz, p.x+sz, p.y+sz); if (rec.tags) { g.setColor(0,0,0); g.setFont("Vector", 18).setFontAlign(-1,-1); g.drawString(rec.tags.name, p.x, p.y); } points ++; } function newLine(tile, a, bin) { let xy = getBin(bin, 0, null); let i = 1; let step = 1; let len = getBinLength(bin); let p1 = toScreen(tile, xy); if (a.properties && a.properties.stroke) { g.setColor(a.properties.stroke); } while (i < len) { xy = getBin(bin, i, xy); var p2 = toScreen(tile, xy); //print(p1.x, p1.y, p2.x, p2.y); g.drawLine(p1.x, p1.y, p2.x, p2.y); if (i == len-1) break; i = i + step; if (i>len) i = len-1; points ++; p1 = p2; } } function newPolygon(tile, a, bin) { let xy = getBin(bin, 0, null); i = 1; step = 1; len = getBinLength(bin); if (len > 62) { step = log2(len) - 5; step = 1<len) i = len-1; points ++; } if (a.properties && a.properties.fill) { g.setColor(a.properties.fill); } else { g.setColor(.75, .75, 1); } g.fillPoly(pol, true); if (a.properties && a.properties.stroke) { g.setColor(a.properties.stroke); } else { g.setColor(0,0,0) } g.drawPoly(pol, true); } function newVector(tile, rec) { let bin = E.toUint8Array(atob(rec.b)); a = meta.attrs[bin[0]]; if (a.type == 1) { newPoint(tile, a, rec, bin); } else if (a.type == 2) { newLine(tile, a, bin); } else if (a.type == 3) { newPolygon(tile, a, bin); } else print("Unknown record", a); g.flip(); } function drawVector(gjson, tile, qual) { var d = gjson; points = 0; var t1 = getTime(); let xy1 = m.latLonToXY(tile[1], tile[0]); let xy2 = m.latLonToXY(tile[3], tile[2]); let t2 = [ xy1.x, xy1.y, xy2.x, xy2.y ]; print(t2); for (var a of d) { // d.features for geojson g.setColor(0,0,0); if (a.type != "Feature") { newVector(t2, a); continue; } // marker-size, marker-color, stroke if (qual < 32 && a.geometry.type == "Point") drawPoint(a); if (qual < 8 && a.geometry.type == "LineString") drawLine(a, qual); if (qual < 8 && a.geometry.type == "Polygon") drawPolygon(a, qual); } print("....", points, "painted in", getTime()-t1, "sec"); } function fname(lon, lat, zoom) { var bbox = [lon, lat, lon, lat]; var r = xyz(bbox, 13, false, "WGS84"); //console.log('fname', r); return 'z'+zoom+'-'+r.minX+'-'+r.minY+'.json'; } function fnames(zoom) { var bb = [m.lon, m.lat, m.lon, m.lat]; var r = xyz(bb, zoom, false, "WGS84"); let maxt = 16; while (1) { var bb2 = bbox(r.minX, r.minY, zoom, false, "WGS84"); var os = m.latLonToXY(bb2[3], bb2[0]); if (os.x >= 0) r.minX -= 1; else if (os.y >= 0) r.minY -= 1; else break; if (!maxt) break; maxt--; } while (1) { var bb2 = bbox(r.maxX, r.maxY, zoom, false, "WGS84"); var os = m.latLonToXY(bb2[1], bb2[2]); if (os.x <= g.getWidth()) r.maxX += 1; else if (os.y <= g.getHeight()) r.maxY += 1; else break; if (!maxt) break; maxt--; } if (!maxt) print("!!! Too many tiles, not painting some"); print(".. paint range", r); return r; } function log2(x) { return Math.log(x) / Math.log(2); } function getZoom(qual) { var z = 16-Math.round(log2(m.scale)); z += qual; z -= 0; if (z < meta.min_zoom) return meta.min_zoom; if (z > meta.max_zoom) return meta.max_zoom; return z; } function drawDebug(text, perc) { g.setClipRect(0,0,R.x2,R.y); g.reset(); g.setColor(1,1,1).fillRect(0,0,R.x2,R.y); g.setColor(1,0,0).fillRect(0,0,R.x2*perc,R.y); g.setColor(0,0,0).setFont("Vector",15); g.setFontAlign(0,0) .drawString(text,80,10); g.setClipRect(R.x,R.y,R.x2,R.y2); g.flip(); } function drawAll(qual) { var zoom = getZoom(qual); var t1 = getTime(); drawDebug("Zoom "+zoom, 0); print("Draw all", m.scale, "->", zoom, "q", qual, "at", m.lat, m.lon); var r = fnames(zoom); var tiles = (r.maxY-r.minY+1) * (r.maxY-r.minY+1); var num = 0; drawDebug("Zoom "+zoom+" tiles "+tiles, 0); for (y=r.minY; y<=r.maxY; y++) { for (x=r.minX; x<=r.maxX; x++) { for (cnt=0; cnt<1000; cnt++) { var n ='z'+zoom+'-'+x+'-'+y+'-'+cnt+'.json'; var gjson = loadVector(n); if (!gjson) break; drawVector(gjson, bbox(x, y, zoom, false, "WGS84"), 1); } num++; drawDebug("Zoom "+zoom+" tiles "+num+"/"+tiles, num/tiles); } } g.flip(); Bangle.drawWidgets(); print("Load and paint in", getTime()-t1, "sec"); } function initVector() { var s = readTarFile("delme.mtar", "meta.json"); meta = JSON.parse(s); } function introScreen() { g.reset().clearRect(R); g.setColor(0,0,0).setFont("Vector",25); g.setFontAlign(0,0); g.drawString("SpaceWeaver", 85,35); g.setColor(0,0,0).setFont("Vector",18); g.drawString("Vector maps", 85,55); g.drawString("Zoom "+meta.min_zoom+".."+meta.max_zoom, 85,75); } m.scale = 76000; m.lat = 50.001; m.lon = 14.759; initVector(); introScreen(); emptyMap();