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 0000000000..0e52fa3168 Binary files /dev/null and b/apps/spacew/app.png differ 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..e07c470491 --- /dev/null +++ b/apps/spacew/prep/minitar.js @@ -0,0 +1,78 @@ +#!/usr/bin/nodejs + +var pc = 1; +var hack = 0; +const hs = require('./heatshrink.js'); + +if (pc) { + fs = require('fs'); + var print=console.log; +} else { + +} + +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); + files = JSON.parse(json); + 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..3d6f81b632 --- /dev/null +++ b/apps/spacew/prep/split.js @@ -0,0 +1,167 @@ +#!/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; + 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]; + 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("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; + var n = `delme/z${z}-${tile.x}-${tile.y}` ; + 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..."