Tangram is designed to work with vector tiles in a number of formats. Data sources are specified in the sources
block of Tangram's scene file. Once a datasource is specified, filters allow you to style different parts of your data in different ways.
The Tangram scene file filters data in two ways: with top-level layer filters and lower-level feature filters.
Layer filters
Vector tiles typically contain top-level structures which can be thought of as "layers" – inside a GeoJSON file, these would be the FeatureCollection objects. Inside a Tangram scene file, the layers
object allows you to split the data by layer, by matching against the layer name.
layers:
my-roads-layer:
data:
source: nextzen
layer: roads
draw: ...
Specifying layer: roads
in the data
block matches this GeoJSON object:
{"roads":
{"type":"FeatureCollection","features":[
{"geometry":"..."}
]}
}
Layer name shortcut
If a layer
filter is not specified, Tangram will attempt to use the layer name as the filter. In this example, the layer name "roads" matches a layer in the data:
layers:
roads:
data:
source: nextzen
draw: ...
Feature filters
Once a top-level layer
filter has been applied, feature-level filter
objects can be defined to filter by feature properties, in order to further narrow down the data of interest and refine the styles applied to the data.
layers:
roads:
data: { source: nextzen }
highway:
filter:
kind: highway
draw: ...
Here, a top-level layer named "roads" matches the "roads" layer in the "osm" data source. It has a style
block, which will apply to all features in the "roads" layer unless it is overridden, functioning as a kind of "default" style.
Then, a sublayer named "highway" is declared, with its own filter
and draw
. Its draw
block will apply only to roads which match its filter
– in this case, those with the property "kind", with a value of "highway".
Inheritance
Higher-level filters continue to apply at lower levels, which means that higher-level draw
parameters will be inherited by lower levels, unless the lower level explicitly changes the draw
parameters.
Using sublayers and inheritance, you may specify increasingly specific filters and draw styles to account for as many special cases as you like.
Matching
Each feature in a layer
is first tested against each top-level filter
, and if the feature's data matches the filter, that feature will be assigned any associated draw
styles, and passed on to any sublayers. If any sublayer filters match the feature, that sublayer's draw
styles will overwrite any previously-assigned styling rules for those matching features, and so on down the chain of inheritance.
Feature filters can match any named feature property in the data, as well as a few special reserved keywords.
Feature properties
Feature properties in a GeoJSON datasource are listed in a JSON member specifically named "properties":
{
"type": "Feature",
"id": "248156318",
"properties": {
"kind": "commercial",
"area": 12148,
"height": 63.4000000
}
Analogous property structures exist in other data formats such as TopoJSON and Mapbox Vector Tiles. Tangram makes these structures available to filter
blocks by property name, and also to any JavaScript filter functions under the feature
keyword.
The json feature above will match these two filters:
filter:
kind: commercial
filter: function() { return feature.kind == "commercial"; }
The simplest type of feature filter is a statement about one named property of a feature.
A filter can match an exact value:
filter:
kind: residential
any value in a list:
filter:
kind: [residential, commercial]
or a value in a numeric range:
filter:
area: { min: 100, max: 500 }
A Boolean value of "true" will pass a feature that contains the named property, ignoring the property's value. A value of "false" will pass a feature that does not contain the named property:
filter:
kind: true
area: false
To match a property whose value is a boolean, use the list syntax:
filter:
boolean_property: [true]
A feature filter can also evaluate one or more properties in a JavaScript function:
filter:
function() { return feature.area > 100000 }
For example, let's say we have a feature with a single property called "height":
{ "type":"Feature", "properties":{ "height":200 } }
This feature will match these filters:
filter: { height: 200 }
filter: { height: { max: 300 } }
filter: { height: true }
filter: { unicycle: false }
filter: function() { return feature.height >= 100; }
filter: function() { return true; }
and will not match these filters:
filter: { height: 100 }
filter: { height: { min: 300 } }
filter: { height: false }
filter: { unicycle: true }
filter: function() { return feature.height <= 100; }
filter: function() { return false; }
Keyword properties
The keyword $geometry
matches the feature's geometry type, for cases when a FeatureCollection includes more than one type of kind of geometry. Valid geometry types are:
point
: matchesPoint
,MultiPoint
line
: matchesLineString
,MultiLineString
polygon
: matchesPolygon
,MultiPolygon
filter: { $geometry: polygon } # matches polygons only
filter: { $geometry: [point, line] } # matches points and lines, but not polygons
filter: function() { return $geometry === 'line' } # matches lines only
The keyword $layer
matches the feature's layer name, for cases when a data layer includes more than one source layer. In the case below, a data layer is created from two source layers, which can then be separated again by layer for styling:
labels:
data: { source: nextzen, layer: [places, pois] }
draw:
...
pois-only:
filter: { $layer: pois } # matches features from the "pois" layer only
draw:
...
The keyword $zoom
matches the current zoom level of the map. It can be used with specific values, or with the min
and max
parameters.
filter: { $zoom: 14 } # matches only zoom 14
filter: { $zoom: { min: 10 } } # matches zooms 10 and up
filter: { $zoom: { min: 12, max: 15 } } # matches zooms 12-14
filter: { $zoom: [8, 9] } # matches only zooms 8 and 9
filter: function() { return $zoom <= 10 } # matches zooms 10 and below
label_placement
The label_placement
property is given only to point geometries created by setting generate_label_centroids
on a datasource to true
. This option creates a new "centroid" point feature in the geometric center of each polygon in the datasource. Then, with label_placement
set to true
, a single label may be drawn in the center of each polygonal feature, instead of one label per tile, which is the default behavior. To add these centroid points to a datasource, add the generate_label_centroids
property to its [source] block:
sources:
nextzen:
type: TopoJSON
url: https://tile.nextzen.org/tilezen/vector/v1/256/all/{z}/{x}/{y}.topojson
generate_label_centroids: true
layers:
landuse:
data: {source: nextzen}
points:
filter:
label_placement: true
Filter functions
Range functions
The filter functions min
and max
are equivalent to >=
and <
in a JavaScript function, and can be used in combination.
filter:
area: { max: 1000 } } # matches areas up to 1000 sq meters
filter:
height: { min: 70 } } # matches heights 70 and up
filter:
$zoom: { min: 5, max: 10 } # matches zooms 5-9
px2
Range functions can also accept a special screen-space area unit called px2
:
filter: { area: { min: 500px2 } }
This example filters the feature's area property by the number of square mercator meters that cover a 500 pixel screen area at the current zoom level. This means that the area property must be in square mercator meters, as the property provided by Mapzen vector tiles is.
As with other pixel-based values in the scene file, the px2
units are expressed in logical pixels (or "CSS pixels"), meaning they are interpreted at a pixel density of 1, and are automatically scaled up for higher density displays.
The px2
unit syntax can be used to simplify more cumbersome per-zoom filters.
Note that a px2
area filter can only be applied if the data source already contains a suitable area property – it does not need to be named area, as any property name can be specified in the filter, but it must already exist in the data source.
Boolean functions
The following Boolean filter functions are also available:
not
any
all
none
(a combination ofnot
andany
)
not
takes a single filter object as its input:
filter:
not: { kind: restaurant }
filter:
not: { kind: [bar, pub] }
any
, all
, and none
take lists of filter objects:
filter:
all:
- { kind: museum }
- function() { return feature.area > 100000 }
filter:
any:
- { height: { min: 100 } }
- { name: true }
filter:
none:
- { kind: cemetery }
- { kind: graveyard }
- { kind: aerodrome }
Lists imply any
, Mappings imply all
A list of several filters is a shortcut for using the any
function. These two filters are equivalent:
filter: [ kind: minor_road, railway: true ]
filter:
any:
- kind: minor_road
- railway: true
A mapping of several filters is a shortcut for using the all
function. These two filters are equivalent:
filter: { kind: hamlet, $zoom: { min: 13 } }
filter:
all:
- kind: hamlet
- $zoom: { min: 13 }
Matching collisions
In some cases, filters at the same level may return overlapping results:
roads:
data: { source: nextzen }
highway:
filter: { kind: highway }
draw: { lines: { color: red } }
bridges:
filter: { is_bridge: yes }
draw: { lines: { color: blue } }
In this case, "highways" are colored red, and "bridges" are blue. However, if any feature is both a "highway" and a "bridge", it will match twice. Because YAML maps are technically "orderless", there's no way to guarantee that one of these styles will consistently be shown over the other. The solution here is to restructure the styles so that each case matches explicitly:
roads:
highway:
filter: { kind: highway }
draw: { lines: { color: red } }
highway-bridges:
filter: { is_bridge: yes }
draw: { lines: { color: blue } }
other-bridges:
filter: { is_bridge: yes, not: { kind: highway} }
draw: { lines: { color: green } }