🎨 implementing mapbox style compiler, turning filter into call chains

This commit is contained in:
Michael Straßburger 2016-09-21 23:40:08 +02:00
parent d7c7500a7f
commit 0877eaca86
5 changed files with 51 additions and 97 deletions

View File

@ -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

View File

@ -3,7 +3,7 @@
No web browser around? No worries - discover the planet in your console!
* 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
* Use an online map server or work offline with VectorTile/MBTiles
* Highly customizable styling (colors, feature visibility, ...)
@ -46,8 +46,11 @@ No web browser around? No worries - discover the planet in your console!
### TODOs
* [ ] cli linking
* [ ] 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
* [ ] support for stroke width
* [ ] support for dashed/dotted lines?
* [ ] label drawing
* [x] support for point 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
* [ ] bbox awareness
* [ ] 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)
* [ ] tile request system
* [ ] 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] accurate mouse drag&drop
* [x] handle console resize
* [ ] styling
* [ ] abstracted MapBox style JSON support
* [x] styling
* [ ] turn this into a [`blessed-contrib`](https://github.com/yaronn/blessed-contrib) widget
## License

View File

@ -20,7 +20,7 @@ module.exports = class Renderer
fillPolygons: true
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:
car: "🚗"
@ -42,29 +42,15 @@ module.exports = class Renderer
cinema: "C" #"🎦"
layers:
housenum_label:
minZoom: 1.5
color: 8
building:
minZoom: 3.8
color: 8
housenum_label: minZoom: 1.5
building: minZoom: 3.8
place_label:
color: "yellow"
place_label: true
poi_label: minZoom: 3
poi_label:
minZoom: 3
color: "yellow"
road:
color: 15
landuse:
color: "green"
water:
color: "blue"
admin:
color: "red"
road: true
water: true
admin: true
isDrawing: false
lastDrawAt: 0
@ -108,6 +94,8 @@ module.exports = class Renderer
@isDrawing = true
@lastDrawAt = Date.now()
@notify "rendering..."
@labelBuffer.clear()
# TODO: better way for background color instead of setting filling FG?
@ -122,16 +110,17 @@ module.exports = class Renderer
@_drawLayers()
@canvas.restore()
@write @canvas._canvas.frame()
@_write @canvas._canvas.frame()
@isDrawing = false
write: (output) ->
_write: (output) ->
process.stdout.write output
_drawLayers: ->
drawn = []
for layer in @config.drawOrder
@notify "rendering #{layer}..."
scale = Math.pow 2, @zoom
continue unless @features?[layer]
@ -240,3 +229,6 @@ module.exports = class Renderer
point.x+@view[0]<@width-4 and
point.y+@view[1]>=0 and
point.y+@view[1]<@height
notify: (text) ->
@_write "\r\x1B[K"+text

View File

@ -5,8 +5,8 @@
Minimalistic parser and compiler for Mapbox (Studio) Map Style files
See: https://www.mapbox.com/mapbox-gl-style-spec/
Verrrrry MVP implementation
TODO: should be optimized by compiling the json to method&cb based filters
Compiles layer filter instructions into a chain of true/false returning
anonymous functions to improve rendering speed compared to realtime parsing.
###
fs = require 'fs'
@ -19,47 +19,48 @@ module.exports = class Styler
json = JSON.parse fs.readFileSync(file).toString()
@styleName = json.name
for layer in json.layers
continue if layer.ref
style = layer
for style in json.layers
continue if style.ref
@styleByLayer[layer['source-layer']] ?= []
@styleByLayer[layer['source-layer']].push style
style.appliesTo = @_compileFilter style.filter
@styleById[layer.id] = style
@styleByLayer[style['source-layer']] ?= []
@styleByLayer[style['source-layer']].push style
@styleById[style.id] = style
getStyleFor: (layer, feature, zoom) ->
# Skip all layers that don't have any styles set
return false unless @styleByLayer[layer]
for style in @styleByLayer[layer]
return style unless style.filter
if @_passesFilter feature, style.filter
return style
return style if style.appliesTo feature
false
_passesFilter: (feature, filter) ->
_compileFilter: (filter) ->
if not filter or not filter.length
return -> true
switch filter[0]
when "all"
for subFilter in filter[1..]
return false unless @_passesFilter feature, subFilter
true
filters = (@_compileFilter subFilter for subFilter in filter[1..])
(feature) ->
return false for appliesTo in filters when not appliesTo feature
true
when "=="
feature.properties[filter[1]] is filter[2]
(feature) -> feature.properties[filter[1]] is filter[2]
when "!="
feature.properties[filter[2]] isnt filter[2]
(feature) -> feature.properties[filter[1]] isnt filter[2]
when "in"
field = filter[1]
for value in filter[2..]
return true if feature.properties[field] is value
false
(feature) ->
return true for value in filter[2..] when feature.properties[filter[1]] is value
false
when "!in"
field = filter[1]
for value in filter[2..]
return false if feature.properties[field] is value
true
(feature) ->
return false for value in filter[2..] when feature.properties[filter[1]] is value
true

View File

@ -119,7 +119,7 @@ module.exports = class Termap
_draw: ->
@renderer.draw @view, @zoom
@renderer.write @_getFooter()
@renderer.notify @_getFooter()
_getBBox: ->
[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}"
# bbox: [#{@_getBBox().map((z) -> utils.digits(z, 2)).join(',')}]"
notify: (text) ->
return if @renderer.isDrawing
@renderer.write "\r\x1B[K#{@_getFooter()} #{text}"
zoomBy: (step) ->
return unless @scale+step > 0