mirror of
https://github.com/rastapasta/mapscii.git
synced 2024-11-22 08:03:07 +01:00
🎨 implementing mapbox style compiler, turning filter into call chains
This commit is contained in:
parent
d7c7500a7f
commit
0877eaca86
@ -1,39 +0,0 @@
|
|||||||
###
|
|
||||||
termap - Terminal Map Viewer
|
|
||||||
by Michael Strassburger <codepoet@cpan.org>
|
|
||||||
|
|
||||||
Using 2D spatial indexing to avoid overlapping labels and markers
|
|
||||||
Future: to detect collision on mouse interaction
|
|
||||||
###
|
|
||||||
|
|
||||||
rbush = require 'rbush'
|
|
||||||
|
|
||||||
module.exports = class LabelBuffer
|
|
||||||
tree: null
|
|
||||||
margin: 5
|
|
||||||
|
|
||||||
constructor: (@width, @height) ->
|
|
||||||
@tree = rbush()
|
|
||||||
|
|
||||||
clear: ->
|
|
||||||
@tree.clear()
|
|
||||||
|
|
||||||
project: (x, y) ->
|
|
||||||
[Math.floor(x/2), Math.floor(y/4)]
|
|
||||||
|
|
||||||
writeIfPossible: (text, x, y) ->
|
|
||||||
point = @project x, y
|
|
||||||
|
|
||||||
if @_hasSpace text, point[0], point[1]
|
|
||||||
@tree.insert @_calculateArea text, point[0], point[1]
|
|
||||||
else
|
|
||||||
false
|
|
||||||
|
|
||||||
_hasSpace: (text, x, y) ->
|
|
||||||
not @tree.collides @_calculateArea text, x, y, 0
|
|
||||||
|
|
||||||
_calculateArea: (text, x, y, margin = @margin) ->
|
|
||||||
minX: x-margin
|
|
||||||
minY: y-margin
|
|
||||||
maxX: x+margin+text.length
|
|
||||||
maxY: y+margin
|
|
10
README.md
10
README.md
@ -3,7 +3,7 @@
|
|||||||
No web browser around? No worries - discover the planet in your console!
|
No web browser around? No worries - discover the planet in your console!
|
||||||
|
|
||||||
* Use your mouse or keys to navigate
|
* Use your mouse or keys to navigate
|
||||||
* Discover the globe or zoom in to learn about house numbers
|
* Discover the globe or zoom in to explore your neighbourhood
|
||||||
* See Point-of-Interest around any given location
|
* See Point-of-Interest around any given location
|
||||||
* Use an online map server or work offline with VectorTile/MBTiles
|
* Use an online map server or work offline with VectorTile/MBTiles
|
||||||
* Highly customizable styling (colors, feature visibility, ...)
|
* Highly customizable styling (colors, feature visibility, ...)
|
||||||
@ -46,8 +46,11 @@ No web browser around? No worries - discover the planet in your console!
|
|||||||
### TODOs
|
### TODOs
|
||||||
* [ ] cli linking
|
* [ ] cli linking
|
||||||
* [ ] mapping of view to tiles to show
|
* [ ] mapping of view to tiles to show
|
||||||
|
* [x] abstracted MapBox style JSON support
|
||||||
|
* [ ] giving render priority to features across layers (collect before render vs. direct)?
|
||||||
* [ ] line drawing
|
* [ ] line drawing
|
||||||
* [ ] support for stroke width
|
* [ ] support for stroke width
|
||||||
|
* [ ] support for dashed/dotted lines?
|
||||||
* [ ] label drawing
|
* [ ] label drawing
|
||||||
* [x] support for point labels
|
* [x] support for point labels
|
||||||
* [x] dynamic decluttering of labels
|
* [x] dynamic decluttering of labels
|
||||||
@ -63,6 +66,8 @@ No web browser around? No worries - discover the planet in your console!
|
|||||||
* [ ] lat/lng-center + zoom based viewport
|
* [ ] lat/lng-center + zoom based viewport
|
||||||
* [ ] bbox awareness
|
* [ ] bbox awareness
|
||||||
* [ ] zoom -> scale calculation
|
* [ ] zoom -> scale calculation
|
||||||
|
* [ ] Tile parsing
|
||||||
|
* [ ] directly throw away features that aren't covered by any style
|
||||||
* [ ] TileSource class (abstracting URL, mbtiles, single vector tile source)
|
* [ ] TileSource class (abstracting URL, mbtiles, single vector tile source)
|
||||||
* [ ] tile request system
|
* [ ] tile request system
|
||||||
* [ ] from local mbtiles
|
* [ ] from local mbtiles
|
||||||
@ -75,8 +80,7 @@ No web browser around? No worries - discover the planet in your console!
|
|||||||
* [x] start with zoom level which shows full vector tile
|
* [x] start with zoom level which shows full vector tile
|
||||||
* [x] accurate mouse drag&drop
|
* [x] accurate mouse drag&drop
|
||||||
* [x] handle console resize
|
* [x] handle console resize
|
||||||
* [ ] styling
|
* [x] styling
|
||||||
* [ ] abstracted MapBox style JSON support
|
|
||||||
* [ ] turn this into a [`blessed-contrib`](https://github.com/yaronn/blessed-contrib) widget
|
* [ ] turn this into a [`blessed-contrib`](https://github.com/yaronn/blessed-contrib) widget
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
@ -20,7 +20,7 @@ module.exports = class Renderer
|
|||||||
fillPolygons: true
|
fillPolygons: true
|
||||||
language: 'de'
|
language: 'de'
|
||||||
|
|
||||||
drawOrder: ["admin", "water", "building", "road", "poi_label", "place_label", "housenum_label"]
|
drawOrder: ["admin", "building", "road", "water", "poi_label", "place_label", "housenum_label"]
|
||||||
|
|
||||||
icons:
|
icons:
|
||||||
car: "🚗"
|
car: "🚗"
|
||||||
@ -42,29 +42,15 @@ module.exports = class Renderer
|
|||||||
cinema: "C" #"🎦"
|
cinema: "C" #"🎦"
|
||||||
|
|
||||||
layers:
|
layers:
|
||||||
housenum_label:
|
housenum_label: minZoom: 1.5
|
||||||
minZoom: 1.5
|
building: minZoom: 3.8
|
||||||
color: 8
|
|
||||||
building:
|
|
||||||
minZoom: 3.8
|
|
||||||
color: 8
|
|
||||||
|
|
||||||
place_label:
|
place_label: true
|
||||||
color: "yellow"
|
poi_label: minZoom: 3
|
||||||
|
|
||||||
poi_label:
|
road: true
|
||||||
minZoom: 3
|
water: true
|
||||||
color: "yellow"
|
admin: true
|
||||||
|
|
||||||
road:
|
|
||||||
color: 15
|
|
||||||
|
|
||||||
landuse:
|
|
||||||
color: "green"
|
|
||||||
water:
|
|
||||||
color: "blue"
|
|
||||||
admin:
|
|
||||||
color: "red"
|
|
||||||
|
|
||||||
isDrawing: false
|
isDrawing: false
|
||||||
lastDrawAt: 0
|
lastDrawAt: 0
|
||||||
@ -108,6 +94,8 @@ module.exports = class Renderer
|
|||||||
@isDrawing = true
|
@isDrawing = true
|
||||||
@lastDrawAt = Date.now()
|
@lastDrawAt = Date.now()
|
||||||
|
|
||||||
|
@notify "rendering..."
|
||||||
|
|
||||||
@labelBuffer.clear()
|
@labelBuffer.clear()
|
||||||
|
|
||||||
# TODO: better way for background color instead of setting filling FG?
|
# TODO: better way for background color instead of setting filling FG?
|
||||||
@ -122,16 +110,17 @@ module.exports = class Renderer
|
|||||||
@_drawLayers()
|
@_drawLayers()
|
||||||
@canvas.restore()
|
@canvas.restore()
|
||||||
|
|
||||||
@write @canvas._canvas.frame()
|
@_write @canvas._canvas.frame()
|
||||||
|
|
||||||
@isDrawing = false
|
@isDrawing = false
|
||||||
|
|
||||||
write: (output) ->
|
_write: (output) ->
|
||||||
process.stdout.write output
|
process.stdout.write output
|
||||||
|
|
||||||
_drawLayers: ->
|
_drawLayers: ->
|
||||||
drawn = []
|
drawn = []
|
||||||
for layer in @config.drawOrder
|
for layer in @config.drawOrder
|
||||||
|
@notify "rendering #{layer}..."
|
||||||
scale = Math.pow 2, @zoom
|
scale = Math.pow 2, @zoom
|
||||||
continue unless @features?[layer]
|
continue unless @features?[layer]
|
||||||
|
|
||||||
@ -240,3 +229,6 @@ module.exports = class Renderer
|
|||||||
point.x+@view[0]<@width-4 and
|
point.x+@view[0]<@width-4 and
|
||||||
point.y+@view[1]>=0 and
|
point.y+@view[1]>=0 and
|
||||||
point.y+@view[1]<@height
|
point.y+@view[1]<@height
|
||||||
|
|
||||||
|
notify: (text) ->
|
||||||
|
@_write "\r\x1B[K"+text
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
Minimalistic parser and compiler for Mapbox (Studio) Map Style files
|
Minimalistic parser and compiler for Mapbox (Studio) Map Style files
|
||||||
See: https://www.mapbox.com/mapbox-gl-style-spec/
|
See: https://www.mapbox.com/mapbox-gl-style-spec/
|
||||||
|
|
||||||
Verrrrry MVP implementation
|
Compiles layer filter instructions into a chain of true/false returning
|
||||||
TODO: should be optimized by compiling the json to method&cb based filters
|
anonymous functions to improve rendering speed compared to realtime parsing.
|
||||||
###
|
###
|
||||||
|
|
||||||
fs = require 'fs'
|
fs = require 'fs'
|
||||||
@ -19,47 +19,48 @@ module.exports = class Styler
|
|||||||
json = JSON.parse fs.readFileSync(file).toString()
|
json = JSON.parse fs.readFileSync(file).toString()
|
||||||
@styleName = json.name
|
@styleName = json.name
|
||||||
|
|
||||||
for layer in json.layers
|
for style in json.layers
|
||||||
continue if layer.ref
|
continue if style.ref
|
||||||
style = layer
|
|
||||||
|
|
||||||
@styleByLayer[layer['source-layer']] ?= []
|
style.appliesTo = @_compileFilter style.filter
|
||||||
@styleByLayer[layer['source-layer']].push style
|
|
||||||
|
|
||||||
@styleById[layer.id] = style
|
@styleByLayer[style['source-layer']] ?= []
|
||||||
|
@styleByLayer[style['source-layer']].push style
|
||||||
|
|
||||||
|
@styleById[style.id] = style
|
||||||
|
|
||||||
getStyleFor: (layer, feature, zoom) ->
|
getStyleFor: (layer, feature, zoom) ->
|
||||||
|
# Skip all layers that don't have any styles set
|
||||||
return false unless @styleByLayer[layer]
|
return false unless @styleByLayer[layer]
|
||||||
|
|
||||||
for style in @styleByLayer[layer]
|
for style in @styleByLayer[layer]
|
||||||
return style unless style.filter
|
return style if style.appliesTo feature
|
||||||
|
|
||||||
if @_passesFilter feature, style.filter
|
|
||||||
return style
|
|
||||||
|
|
||||||
false
|
false
|
||||||
|
|
||||||
_passesFilter: (feature, filter) ->
|
_compileFilter: (filter) ->
|
||||||
|
if not filter or not filter.length
|
||||||
|
return -> true
|
||||||
|
|
||||||
switch filter[0]
|
switch filter[0]
|
||||||
when "all"
|
when "all"
|
||||||
for subFilter in filter[1..]
|
filters = (@_compileFilter subFilter for subFilter in filter[1..])
|
||||||
return false unless @_passesFilter feature, subFilter
|
(feature) ->
|
||||||
|
return false for appliesTo in filters when not appliesTo feature
|
||||||
true
|
true
|
||||||
|
|
||||||
when "=="
|
when "=="
|
||||||
feature.properties[filter[1]] is filter[2]
|
(feature) -> feature.properties[filter[1]] is filter[2]
|
||||||
|
|
||||||
when "!="
|
when "!="
|
||||||
feature.properties[filter[2]] isnt filter[2]
|
(feature) -> feature.properties[filter[1]] isnt filter[2]
|
||||||
|
|
||||||
when "in"
|
when "in"
|
||||||
field = filter[1]
|
(feature) ->
|
||||||
for value in filter[2..]
|
return true for value in filter[2..] when feature.properties[filter[1]] is value
|
||||||
return true if feature.properties[field] is value
|
|
||||||
false
|
false
|
||||||
|
|
||||||
when "!in"
|
when "!in"
|
||||||
field = filter[1]
|
(feature) ->
|
||||||
for value in filter[2..]
|
return false for value in filter[2..] when feature.properties[filter[1]] is value
|
||||||
return false if feature.properties[field] is value
|
|
||||||
true
|
true
|
||||||
|
@ -119,7 +119,7 @@ module.exports = class Termap
|
|||||||
|
|
||||||
_draw: ->
|
_draw: ->
|
||||||
@renderer.draw @view, @zoom
|
@renderer.draw @view, @zoom
|
||||||
@renderer.write @_getFooter()
|
@renderer.notify @_getFooter()
|
||||||
|
|
||||||
_getBBox: ->
|
_getBBox: ->
|
||||||
[x, y] = mercator.forward [@center.lng, @center.lat]
|
[x, y] = mercator.forward [@center.lng, @center.lat]
|
||||||
@ -132,10 +132,6 @@ module.exports = class Termap
|
|||||||
"center: [#{utils.digits @center.lat, 2}, #{utils.digits @center.lng, 2}] zoom: #{utils.digits @zoom, 2}"
|
"center: [#{utils.digits @center.lat, 2}, #{utils.digits @center.lng, 2}] zoom: #{utils.digits @zoom, 2}"
|
||||||
# bbox: [#{@_getBBox().map((z) -> utils.digits(z, 2)).join(',')}]"
|
# bbox: [#{@_getBBox().map((z) -> utils.digits(z, 2)).join(',')}]"
|
||||||
|
|
||||||
notify: (text) ->
|
|
||||||
return if @renderer.isDrawing
|
|
||||||
@renderer.write "\r\x1B[K#{@_getFooter()} #{text}"
|
|
||||||
|
|
||||||
zoomBy: (step) ->
|
zoomBy: (step) ->
|
||||||
return unless @scale+step > 0
|
return unless @scale+step > 0
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user