From 3158d945aaba454cf40c5901f96a94bed8fb39d7 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Fri, 18 Aug 2023 14:17:53 +0200 Subject: [PATCH 1/2] Add Space Weaver -- vector map application. This really needs more work (as documented in README), but this is already quite useful. Please check app.js file -- I used library for conversion between xyz and lat/lon, so its license applies. It seems to be compatible with bangle apps license. --- apps/spacew/README.md | 43 +++ apps/spacew/app-icon.js | 2 + apps/spacew/app.js | 620 ++++++++++++++++++++++++++++++++++ apps/spacew/app.png | Bin 0 -> 3263 bytes apps/spacew/metadata.json | 13 + apps/spacew/prep/minitar.js | 82 +++++ apps/spacew/prep/prepare.json | 18 + apps/spacew/prep/prepare.sh | 18 + apps/spacew/prep/split.js | 177 ++++++++++ apps/spacew/prep/stats.sh | 22 ++ 10 files changed, 995 insertions(+) create mode 100644 apps/spacew/README.md create mode 100644 apps/spacew/app-icon.js create mode 100644 apps/spacew/app.js create mode 100644 apps/spacew/app.png create mode 100644 apps/spacew/metadata.json create mode 100755 apps/spacew/prep/minitar.js create mode 100644 apps/spacew/prep/prepare.json create mode 100755 apps/spacew/prep/prepare.sh create mode 100755 apps/spacew/prep/split.js create mode 100755 apps/spacew/prep/stats.sh diff --git a/apps/spacew/README.md b/apps/spacew/README.md new file mode 100644 index 0000000000..4f2ca3f003 --- /dev/null +++ b/apps/spacew/README.md @@ -0,0 +1,43 @@ +# Space Weaver ![](app.png) + +Vector map + +Written by: [Pavel Machek](https://github.com/pavelmachek) + +Space Weaver is application for displaying vector maps. It is +currently suitable for developers, and more work is needed. + +Maps can be created from openstreetmap extracts. Those are cut using +osmosis, then translated into geojson. Geojson is further processes to +add metadata such as colors, and to split it into xyz tiles, while +keeping geojson format. Tiles are then merged into single file, which +can be uploaded to the filesystem. Index at the end provides locations +of the tiles. + +## Preparing data + +Tools in spacew/prep can be used to prepare data. + +You'll need to edit prepare.sh to point it to suitable osm extract, +and you'll need to select area of interest. Start experiments with +small area. You may want to delete cstocs and provide custom +conversion to ascii. + +Details of which features are visible at what zoom levels can be +configured in split.js. This can greatly affect file sizes. Then +there's "meta.max_zoom = 17" setting, reduce it if file is too big. + +For initial experiments, configure things so that mtar file is around +500KB. (I had troubles with big files, both on hardware and to lesser +extent on simulator. In particular, mtar seemed to be corrupted after +emulator window was closed.) + +## Future Development + +Directories at the end of .mtar should be hashed, not linear searched. + +Geojson is not really suitable as it takes a lot of storage. + +It would be nice to support polygons. + +Web-based tool for preparing maps would be nice. \ No newline at end of file diff --git a/apps/spacew/app-icon.js b/apps/spacew/app-icon.js new file mode 100644 index 0000000000..27b6e2662a --- /dev/null +++ b/apps/spacew/app-icon.js @@ -0,0 +1,2 @@ +require("heatshrink").decompress(atob("mEwwkE/8Ql//j//AAUD//yAgILBAAXzBQMxAoMwn4XKBIgXCmAXEh4XF+IJGC4XxAoMgl/zgX/nASBBgPwIoIEBmYBBI4ug1/6hX/zOf+UBEIMP+UC+eZAIPyhP/yAnB0Ef+QXBnM/GgUwh4ECwX/wYvCkIvB+BrBA4JsFAQMiL4gRBA4XxNYIlBBgQGBiJXEBQRnBiYoEiQXFgURT4YAB+QXBS4RTCJoQMBj4gBWQPwN4IKCNgLHDRAIlDEgIxBC4zHBJITACC4gMB+MfAIJCCRIU/GIIGCEoLyCBgQOCgZAEBAL5CC4UvC4oFBMIJ9CCAQMBPwbABKoYMBJ4KpBZQgKBVwnyh/wKoQMBVoUgn4XFmTGEgfxC4QKBCQRKBeAYtBkYXFXYIFBkTfCSgMfIIYbBdwTADJIIEBkYEDAYKyDC4J9DKoSFDiZMDGYKCDkbWEKoUzIQQREHQIFDifzBQYXGIIIMDkDwDN4IXFIIIXBJQMhEQqCCT4IWENoUCC4MvXwTjCiZqBEQIXGNoITBC4LRDEQMDHQbWEAAUDIYPzmabEEQIXDmYXGiUgFAMyLASQDXgPzj7uEQobNB+MxWYsgHQKSBEQqFCUYPwUwgKCHQUvEQqFCkAXCBQ0Qn/xmYXH+IXB+S+ESAUAEQMzHQqFCgEvmS+EBQUBl/wUw4MDmS+ESAcf+ExC44MCmS+ESAcPmAvI/8hh8iNY8wgcwaw4MCh8hNY/wC5kDTwKbHgThGEgsQQZMhdw61CgSmGAAUANRAkCgUTBZEQiRSHHga+HNYUCC5I8BXw4XCgIWJHgJTJ+IXJHAIXB+eTJoIJD+fyC4LABYQWZBQOYC4Mf+eS/85DgIJBxMygAFB+YUBC4YqBkAoBAIM5n4JCAgIvBwYBCNgyDKTRIXM+YXFA=")) + diff --git a/apps/spacew/app.js b/apps/spacew/app.js new file mode 100644 index 0000000000..e438799f6d --- /dev/null +++ b/apps/spacew/app.js @@ -0,0 +1,620 @@ +/* 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 (31 sec)"); + } + }; + if (fix.fix) menu[/*LANG*/"Center GPS"]=() =>{ + m.lat = fix.lat; + m.lon = fix.lon; + showMap(); + }; + E.showMenu(menu); + }}); +} + +var gjson = null; + +function readTarFile(tar, f) { + const st = require('Storage'); + json_off = st.read(tar, 0, 16) * 1; + if (isNaN(json_off)) { + print("Don't have archive", tar); + return undefined; + } + while (1) { + json_len = st.read(tar, json_off, 6) * 1; + if (json_len == -1) + break; + json_off += 6; + json = st.read(tar, json_off, json_len); + //print("Have directory, ", json.length, "bytes"); + //print(json); + files = JSON.parse(json); + //print(files); + rec = files[f]; + if (rec) + return st.read(tar, rec.st, rec.si); + json_off += json_len; + } + return undefined; +} + +function loadVector(name) { + var t1 = getTime(); + print(".. Read", name); + //s = require("Storage").read(name); + var s = readTarFile("delme.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) { + 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; + g.flip(); + } +} + +function drawVector(gjson, qual) { + var d = gjson; + points = 0; + var t1 = getTime(); + + for (var a of d.features) { + if (a.type != "Feature") + print("Expecting feature"); + g.setColor(0,0,0); + // marker-size, marker-color, stroke + if (qual < 32 && a.geometry.type == "Point") + drawPoint(a); + if (qual < 8 && a.geometry.type == "LineString") + drawLine(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"); + 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; + } + 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; + } + 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, 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 = 76; +m.lat = 50.001; +m.lon = 14.759; + +initVector(); +introScreen(); +emptyMap(); diff --git a/apps/spacew/app.png b/apps/spacew/app.png new file mode 100644 index 0000000000000000000000000000000000000000..0e52fa31684acc82bb0262f4fa05e31ef56151a3 GIT binary patch literal 3263 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 literal 0 HcmV?d00001 diff --git a/apps/spacew/metadata.json b/apps/spacew/metadata.json new file mode 100644 index 0000000000..51bdb35b87 --- /dev/null +++ b/apps/spacew/metadata.json @@ -0,0 +1,13 @@ +{ "id": "spacew", + "name": "Space Weaver", + "version":"0.01", + "description": "Application for displaying vector maps", + "icon": "app.png", + "readme": "README.md", + "supports" : ["BANGLEJS2"], + "tags": "outdoors,gps,osm", + "storage": [ + {"name":"spacew.app.js","url":"app.js"}, + {"name":"spacew.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/spacew/prep/minitar.js b/apps/spacew/prep/minitar.js new file mode 100755 index 0000000000..bf59e7a646 --- /dev/null +++ b/apps/spacew/prep/minitar.js @@ -0,0 +1,82 @@ +#!/usr/bin/nodejs + +var pc = 1; +var hack = 0; +const hs = require('./heatshrink.js'); + +if (pc) { + fs = require('fs'); + var print=console.log; +} else { + +} + +print("hello world"); + +function writeDir(json) { + json_str = JSON.stringify(json, "", " "); + dirent = '' + json_str.length; + while (dirent.length < 6) + dirent = dirent + ' '; + return dirent + json_str; +} + +function writeTar(tar, dir) { + var h_len = 16; + var cur = h_len; + files = fs.readdirSync(dir); + data = ''; + var directory = ''; + var json = {}; + for (f of files) { + d = fs.readFileSync(dir+f); + cs = d; + //cs = String.fromCharCode.apply(null, hs.compress(d)) + print("Processing", f, cur, d.length, cs.length); + //if (d.length == 42) continue; + data = data + cs; + var f_rec = {}; + f_rec.st = cur; + var len = d.length; + f_rec.si = len; + cur = cur + len; + json[f] = f_rec; + json_str = JSON.stringify(json, "", " "); + if (json_str.length < 16000) + continue; + directory += writeDir(json); + json = {}; + } + directory += writeDir(json); + directory += '-1 '; + + size = cur; + header = '' + size; + while (header.length < h_len) { + header = header+' '; + } + if (!hack) + fs.writeFileSync(tar, header+data+directory); + else + fs.writeFileSync(tar, directory); +} + +function readTarFile(tar, f) { + const st = require('Storage'); + json_off = st.read(tar, 0, 16) * 1; + print(json_off); + json = st.read(tar, json_off, -1); + //print(json); + files = JSON.parse(json); + //print(files); + rec = files[f]; + return st.read(tar, rec.st, rec.si); +} + +if (pc) + writeTar("delme.mtaz", "delme/"); +else { + print(readTarFile("delme.mtar", "ahoj")); + print(readTarFile("delme.mtar", "nazdar")); +} + diff --git a/apps/spacew/prep/prepare.json b/apps/spacew/prep/prepare.json new file mode 100644 index 0000000000..33cb21a3c1 --- /dev/null +++ b/apps/spacew/prep/prepare.json @@ -0,0 +1,18 @@ +{ + "attributes": { + "type": false, + "id": false, + "version": false, + "changeset": false, + "timestamp": false, + "uid": false, + "user": false, + "way_nodes": false, + }, + "format_options": { + }, + "linear_tags": true, + "area_tags": false, + "exclude_tags": [], + "include_tags": [ "place", "name", "landuse", "highway" ] +} diff --git a/apps/spacew/prep/prepare.sh b/apps/spacew/prep/prepare.sh new file mode 100755 index 0000000000..ac1db00198 --- /dev/null +++ b/apps/spacew/prep/prepare.sh @@ -0,0 +1,18 @@ +#!/bin/bash +if [ ".$1" == "-f" ]; then + I=/data/gis/osm/dumps/czech_republic-2023-07-24.osm.pbf + #I=/data/gis/osm/dumps/zernovka.osm.bz2 + O=cr.geojson + rm delme.pbf $O + time osmium extract $I --bbox 14.7,49.9,14.8,50.1 -f pbf -o delme.pbf + time osmium export delme.pbf -c prepare.json -o $O + echo "Converting to ascii" + time cstocs utf8 ascii cr.geojson > cr_ascii.geojson + mv -f cr_ascii.geojson delme.json +fi +rm -r delme/; mkdir delme +./split.js +./minitar.js +ls -lS delme/*.json | head -20 +cat delme/* | wc -c +ls -l delme.mtar diff --git a/apps/spacew/prep/split.js b/apps/spacew/prep/split.js new file mode 100755 index 0000000000..f25e8e338f --- /dev/null +++ b/apps/spacew/prep/split.js @@ -0,0 +1,177 @@ +#!/usr/bin/nodejs --max-old-space-size=5500 + +// npm install geojson-vt +// docs: https://github.com/mapbox/geojson-vt +// output format: https://github.com/mapbox/vector-tile-spec/ + +const fs = require('fs'); +const sphm = require('./sphericalmercator.js'); +var split = require('geojson-vt') + +// delme.json needs to be real file, symlink to geojson will not work +console.log("Loading json"); +var gjs = require("./delme.json"); + +function tileToLatLon(x, y, z, x_, y_) { + var [ w, s, e, n ] = merc.bbox(x, y, z); + var lon = (e - w) * (x_ / 4096) + w; + var lat = (n - s) * (1-(y_ / 4096)) + s; + //console.log("to ", lon, lat); + return [ lon, lat ]; +} + +function convGeom(tile, geom) { + var g = []; + for (i = 0; i< geom.length; i++) { + var x = geom[i][0]; + var y = geom[i][1]; + //console.log("Geometry: ", geom, geom.length, "X,y", x, y); + var pos = tileToLatLon(tile.x, tile.y, tile.z, x, y); + g.push(pos); + } + return g; +} + +function zoomPoint(tags) { + var z = 99; + + if (tags.place == "city") z = 4; + if (tags.place == "town") z = 8; + if (tags.place == "village") z = 10; + + return z; +} + +function paintPoint(tags) { + var p = {}; + + if (tags.place == "village") p["marker-color"] = "#ff0000"; + + return p; +} + +function zoomWay(tags) { + var z = 99; + + if (tags.highway == "motorway") z = 7; + if (tags.highway == "primary") z = 9; + if (tags.highway == "secondary") z = 13; + if (tags.highway == "tertiary") z = 14; + if (tags.highway == "unclassified") z = 16; + if (tags.highway == "residential") z = 17; + if (tags.highway == "track") z = 17; + if (tags.highway == "path") z = 17; + if (tags.highway == "footway") z = 17; + + return z; +} + +function paintWay(tags) { + var p = {}; + + if (tags.highway == "motorway" || tags.highway == "primary") /* ok */; + if (tags.highway == "secondary" || tags.highway == "tertiary") p.stroke = "#0000ff"; + if (tags.highway == "tertiary" || tags.highway == "unclassified" || tags.highway == "residential") p.stroke = "#00ff00"; + if (tags.highway == "track") p.stroke = "#ff0000"; + if (tags.highway == "path" || tags.highway == "footway") p.stroke = "#800000"; + + return p; +} + +function writeFeatures(name, feat) +{ + var n = {}; + n.type = "FeatureCollection"; + n.features = feat; + + fs.writeFile(name+'.json', JSON.stringify(n), on_error); +} + +function toGjson(name, d, tile) { + var cnt = 0; + var feat = []; + for (var a of d) { + var f = {}; + var zoom = 99; + var p = {}; + f.properties = a.tags; + f.type = "Feature"; + f.geometry = {}; + if (a.type == 1) { + f.geometry.type = "Point"; + f.geometry.coordinates = convGeom(tile, a.geometry)[0]; + zoom = zoomPoint(a.tags); + p = paintPoint(a.tags); + } else if (a.type == 2) { + f.geometry.type = "LineString"; + f.geometry.coordinates = convGeom(tile, a.geometry[0]); + zoom = zoomWay(a.tags); + p = paintWay(a.tags); + } else { + //console.log("Unknown type", a.type); + } + //zoom -= 4; // Produces way nicer map, at expense of space. + if (tile.z < zoom) + continue; + f.properties = Object.assign({}, f.properties, p); + feat.push(f); + var s = JSON.stringify(feat); + if (s.length > 6000) { + console.log("tile too big, splitting", cnt); + writeFeatures(name+'-'+cnt++, feat); + feat = []; + } + } + writeFeatures(name+'-'+cnt, feat); + return n; +} + +function writeTile(name, d, tile) { + toGjson(name, d, tile) +} + +// By default, precomputes up to z30 +var merc = new sphm({ + size: 256, + antimeridian: true +}); + +//console.log(merc.ll([124, 123], 15)); +//console.log(merc.px([17734, 11102], 15)); +//console.log(merc.bbox(17734, 11102, 15)); +//return; + +console.log("Splitting data"); +var meta = {} +meta.min_zoom = 0; +meta.max_zoom = 17; // HERE + // = 16 ... split3 takes > 30 minutes + // = 13 ... 2 minutes +var index = split(gjs, Object.assign({ + maxZoom: meta.max_zoom, + indexMaxZoom: meta.max_zoom, + indexMaxPoints: 0, + tolerance: 30, +}), {}); +console.log("Producing output"); + +var output = {}; + +function on_error(e) { + if (e) { console.log(e); } +} + +var num = 0; +for (const id in index.tiles) { + const tile = index.tiles[id]; + const z = tile.z; + console.log(num++, ":", tile.x, tile.y, z); + var d = index.getTile(z, tile.x, tile.y).features; + //console.log(d); + var n = `delme/z${z}-${tile.x}-${tile.y}` ; + //output[n] = d; + //console.log(n); + writeTile(n, d, tile) +} + +fs.writeFile('delme/meta.json', JSON.stringify(meta), on_error); diff --git a/apps/spacew/prep/stats.sh b/apps/spacew/prep/stats.sh new file mode 100755 index 0000000000..6c10ea1b04 --- /dev/null +++ b/apps/spacew/prep/stats.sh @@ -0,0 +1,22 @@ +#!/bin/bash +zoom() { + echo "Zoom $1" + cat delme/z$1-* | wc -c + echo "M..k..." +} + +echo "Total data" +cat delme/* | wc -c +echo "M..k..." +zoom 18 +zoom 17 +zoom 16 +zoom 15 +zoom 14 +zoom 13 +zoom 12 +zoom 11 +zoom 10 +echo "Zoom 1..9" +cat delme/z?-* | wc -c +echo "M..k..." From 1c96a66db92b2694830390afbfdbc166d39f8703 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Fri, 18 Aug 2023 22:49:30 +0200 Subject: [PATCH 2/2] Fix tabs-vs-spaces warning, remove extra debugging. --- apps/spacew/prep/minitar.js | 46 ++++++++++------------ apps/spacew/prep/split.js | 78 ++++++++++++++++--------------------- 2 files changed, 55 insertions(+), 69 deletions(-) diff --git a/apps/spacew/prep/minitar.js b/apps/spacew/prep/minitar.js index bf59e7a646..e07c470491 100755 --- a/apps/spacew/prep/minitar.js +++ b/apps/spacew/prep/minitar.js @@ -11,13 +11,11 @@ if (pc) { } -print("hello world"); - function writeDir(json) { json_str = JSON.stringify(json, "", " "); dirent = '' + json_str.length; while (dirent.length < 6) - dirent = dirent + ' '; + dirent = dirent + ' '; return dirent + json_str; } @@ -29,23 +27,23 @@ function writeTar(tar, dir) { var directory = ''; var json = {}; for (f of files) { - d = fs.readFileSync(dir+f); - cs = d; - //cs = String.fromCharCode.apply(null, hs.compress(d)) - print("Processing", f, cur, d.length, cs.length); - //if (d.length == 42) continue; - data = data + cs; - var f_rec = {}; - f_rec.st = cur; - var len = d.length; - f_rec.si = len; - cur = cur + len; - json[f] = f_rec; - json_str = JSON.stringify(json, "", " "); - if (json_str.length < 16000) - continue; - directory += writeDir(json); - json = {}; + d = fs.readFileSync(dir+f); + cs = d; + //cs = String.fromCharCode.apply(null, hs.compress(d)) + print("Processing", f, cur, d.length, cs.length); + //if (d.length == 42) continue; + data = data + cs; + var f_rec = {}; + f_rec.st = cur; + var len = d.length; + f_rec.si = len; + cur = cur + len; + json[f] = f_rec; + json_str = JSON.stringify(json, "", " "); + if (json_str.length < 16000) + continue; + directory += writeDir(json); + json = {}; } directory += writeDir(json); directory += '-1 '; @@ -53,12 +51,12 @@ function writeTar(tar, dir) { size = cur; header = '' + size; while (header.length < h_len) { - header = header+' '; + header = header+' '; } if (!hack) - fs.writeFileSync(tar, header+data+directory); + fs.writeFileSync(tar, header+data+directory); else - fs.writeFileSync(tar, directory); + fs.writeFileSync(tar, directory); } function readTarFile(tar, f) { @@ -66,9 +64,7 @@ function readTarFile(tar, f) { json_off = st.read(tar, 0, 16) * 1; print(json_off); json = st.read(tar, json_off, -1); - //print(json); files = JSON.parse(json); - //print(files); rec = files[f]; return st.read(tar, rec.st, rec.si); } diff --git a/apps/spacew/prep/split.js b/apps/spacew/prep/split.js index f25e8e338f..3d6f81b632 100755 --- a/apps/spacew/prep/split.js +++ b/apps/spacew/prep/split.js @@ -16,18 +16,16 @@ function tileToLatLon(x, y, z, x_, y_) { var [ w, s, e, n ] = merc.bbox(x, y, z); var lon = (e - w) * (x_ / 4096) + w; var lat = (n - s) * (1-(y_ / 4096)) + s; - //console.log("to ", lon, lat); return [ lon, lat ]; } function convGeom(tile, geom) { var g = []; for (i = 0; i< geom.length; i++) { - var x = geom[i][0]; - var y = geom[i][1]; - //console.log("Geometry: ", geom, geom.length, "X,y", x, y); - var pos = tileToLatLon(tile.x, tile.y, tile.z, x, y); - g.push(pos); + var x = geom[i][0]; + var y = geom[i][1]; + var pos = tileToLatLon(tile.x, tile.y, tile.z, x, y); + g.push(pos); } return g; } @@ -91,36 +89,36 @@ function toGjson(name, d, tile) { var cnt = 0; var feat = []; for (var a of d) { - var f = {}; - var zoom = 99; - var p = {}; - f.properties = a.tags; - f.type = "Feature"; - f.geometry = {}; - if (a.type == 1) { - f.geometry.type = "Point"; - f.geometry.coordinates = convGeom(tile, a.geometry)[0]; - zoom = zoomPoint(a.tags); - p = paintPoint(a.tags); - } else if (a.type == 2) { - f.geometry.type = "LineString"; - f.geometry.coordinates = convGeom(tile, a.geometry[0]); - zoom = zoomWay(a.tags); - p = paintWay(a.tags); - } else { - //console.log("Unknown type", a.type); - } - //zoom -= 4; // Produces way nicer map, at expense of space. - if (tile.z < zoom) - continue; - f.properties = Object.assign({}, f.properties, p); - feat.push(f); - var s = JSON.stringify(feat); - if (s.length > 6000) { - console.log("tile too big, splitting", cnt); - writeFeatures(name+'-'+cnt++, feat); - feat = []; - } + var f = {}; + var zoom = 99; + var p = {}; + f.properties = a.tags; + f.type = "Feature"; + f.geometry = {}; + if (a.type == 1) { + f.geometry.type = "Point"; + f.geometry.coordinates = convGeom(tile, a.geometry)[0]; + zoom = zoomPoint(a.tags); + p = paintPoint(a.tags); + } else if (a.type == 2) { + f.geometry.type = "LineString"; + f.geometry.coordinates = convGeom(tile, a.geometry[0]); + zoom = zoomWay(a.tags); + p = paintWay(a.tags); + } else { + //console.log("Unknown type", a.type); + } + //zoom -= 4; // Produces way nicer map, at expense of space. + if (tile.z < zoom) + continue; + f.properties = Object.assign({}, f.properties, p); + feat.push(f); + var s = JSON.stringify(feat); + if (s.length > 6000) { + console.log("tile too big, splitting", cnt); + writeFeatures(name+'-'+cnt++, feat); + feat = []; + } } writeFeatures(name+'-'+cnt, feat); return n; @@ -136,11 +134,6 @@ var merc = new sphm({ antimeridian: true }); -//console.log(merc.ll([124, 123], 15)); -//console.log(merc.px([17734, 11102], 15)); -//console.log(merc.bbox(17734, 11102, 15)); -//return; - console.log("Splitting data"); var meta = {} meta.min_zoom = 0; @@ -167,10 +160,7 @@ for (const id in index.tiles) { const z = tile.z; console.log(num++, ":", tile.x, tile.y, z); var d = index.getTile(z, tile.x, tile.y).features; - //console.log(d); var n = `delme/z${z}-${tile.x}-${tile.y}` ; - //output[n] = d; - //console.log(n); writeTile(n, d, tile) }