🎨 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! 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

View File

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

View File

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

View File

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