Skip to content

Commit 4caaedc

Browse files
kevinkreiserpre-commit-ci[bot]kkreiser-sc
authored
Support for On-The-Fly GeoJson Layers and a new Mutator Transform (#2095)
* first stab at external layers and mutator transform * change name to landmarks * rename test as pr number * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * lint my changes let others unlinted * move landmarks to official layer and rely on other tile queue changes to load inline layers * dont need special fixture env for landmarks testing * rename layer_path, update mutator error text and change test to expect non string values * origin coords for landmark pois can be in list form as we dont send them downstream anyway * transforms must be re-entrant, since this one destructively modifies, take a copy * match proper kinds vernacular * more nomenclature changes Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Kevin Kreiser <kkreiser@snapchat.com>
1 parent 61dddbc commit 4caaedc

File tree

5 files changed

+153
-1
lines changed

5 files changed

+153
-1
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# -*- encoding: utf-8 -*-
2+
import json
3+
import math
4+
import os
5+
import shutil
6+
7+
import dsl
8+
from shapely.wkt import loads as wkt_loads
9+
10+
from . import FixtureTest
11+
12+
13+
class InlineLayers(FixtureTest):
14+
def test_no_landmarks_buildings_merged(self):
15+
"""
16+
First make sure that even if there is no external layer everything still loads fine and the buildings do merge
17+
"""
18+
# just in case a failed test didnt cleanup
19+
shutil.rmtree('metatile-layers', ignore_errors=True)
20+
21+
# get two adjacent buildings there so they will merge
22+
self.generate_fixtures(dsl.way(777, wkt_loads('POLYGON ((0 0, 0 .001, .001 .001, .001 0, 0 0))'),
23+
{u'building': u'yes', u'layer': u'2', u'name': u'foo'}),
24+
dsl.way(778, wkt_loads('POLYGON ((.001 0, .001 .001, .002 .001, .002 0, .001 0))'),
25+
{u'building': u'yes', u'layer': u'2', u'name': u'bar'}),
26+
)
27+
28+
# prove they merged, there should be 1 building there
29+
self.assert_n_matching_features(15, 16384, 16383, 'buildings', {'kind': 'building'}, 1)
30+
31+
# prove there are no auxiliary buildings pois
32+
self.assert_no_matching_feature(15, 16384, 16383, 'landmarks', {})
33+
34+
def test_landmarks_buildings_unmerged(self):
35+
# we can temporarily add the external layer, here we build a square structure over the center of the map
36+
# note that the origin of the feature is in the upper right quadrant but should appear in all quadrants
37+
null_island = {
38+
'type': 'FeatureCollection',
39+
'name': 'foo',
40+
'crs': {'type': 'name', 'properties': {'name': 'urn:ogc:def:crs:EPSG::3857'}},
41+
'features': [
42+
{'type': 'Feature',
43+
'properties': {'name': 'null island hut', 'supersede': True, 'height': math.pi, 'origin': [1, 1], 'id': 42},
44+
'geometry': {'type': 'Polygon', 'coordinates':
45+
[[[111, 111], [-111, 111], [-111, -111], [111, -111], [111, 111]]]}}
46+
]
47+
}
48+
49+
# write it to disk
50+
shutil.rmtree('metatile-layers', ignore_errors=True)
51+
os.makedirs('metatile-layers')
52+
with open('metatile-layers/landmarks.geojson', 'w') as f:
53+
json.dump(null_island, f)
54+
55+
# get a building or two in there right next to each other so they will merge
56+
self.generate_fixtures(dsl.way(777, wkt_loads('POLYGON ((0 0, 0 .001, .001 .001, .001 0, 0 0))'),
57+
{u'building': u'yes', u'layer': u'2', u'name': u'foo'}),
58+
dsl.way(778, wkt_loads('POLYGON ((.001 0, .001 .001, .002 .001, .002 0, .001 0))'),
59+
{u'building': u'yes', u'layer': u'2', u'name': u'bar'}),
60+
)
61+
62+
# prove they didnt merge, there should still be 2 buildings there
63+
self.assert_n_matching_features(15, 16384, 16383, 'buildings', {'kind': 'building'}, 2)
64+
65+
# check that the property was copied to the right building
66+
self.assert_has_feature(17, 65536, 65535, 'buildings', {'superseded_by': True, 'name': 'foo'})
67+
68+
# check that the pois made it in properly mutated from the original feature
69+
props = {'name': 'null island hut', 'height': math.pi, 'id': 42}
70+
self.assert_has_feature(15, 16383, 16383, 'landmarks', props)
71+
self.assert_feature_geom_type(15, 16383, 16383, 'landmarks', 42, 'Point')
72+
self.assert_has_feature(15, 16384, 16383, 'landmarks', props)
73+
self.assert_feature_geom_type(15, 16384, 16383, 'landmarks', 42, 'Point')
74+
self.assert_has_feature(15, 16383, 16384, 'landmarks', props)
75+
self.assert_feature_geom_type(15, 16383, 16384, 'landmarks', 42, 'Point')
76+
self.assert_has_feature(15, 16384, 16384, 'landmarks', props)
77+
self.assert_feature_geom_type(15, 16384, 16384, 'landmarks', 42, 'Point')
78+
79+
# clean up external layer
80+
shutil.rmtree('metatile-layers')

integration-test/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@
3939
from tilequeue.tile import coord_to_mercator_bounds
4040
from tilequeue.tile import reproject_lnglat_to_mercator
4141
from tilequeue.tile import reproject_mercator_to_lnglat
42-
from yaml import load as load_yaml
4342

4443
from vectordatasource.meta import find_yaml_path
4544
from vectordatasource.meta.python import make_function_name_min_zoom
4645
from vectordatasource.meta.python import make_function_name_props
4746
from vectordatasource.meta.python import output_kind
4847
from vectordatasource.meta.python import output_min_zoom
4948
from vectordatasource.meta.python import parse_layers
49+
from yaml import load as load_yaml
5050

5151

5252
# the Overpass server is used to download data about OSM elements. the

queries.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ all:
99
- boundaries
1010
- transit
1111
- admin_areas
12+
- landmarks
1213

1314
sources:
1415

@@ -338,6 +339,15 @@ layers:
338339
- vectordatasource.transform.add_id_to_properties
339340
- vectordatasource.transform.remove_feature_id
340341
area-inclusion-threshold: 1
342+
# optional inline layer, if the file is found on disk it will be used
343+
landmarks:
344+
geometry_types: [Point, Polygon, MultiPolygon]
345+
clip: false
346+
area-inclusion-threshold: 1
347+
simplify_before_intersect: false
348+
simplify_start: 0,
349+
pre_processed_layer_path: ./metatile-layers/landmarks.geojson
350+
341351
post_process:
342352
- fn: vectordatasource.transform.build_fence
343353
params:
@@ -542,6 +552,23 @@ post_process:
542552
source_layer: roads
543553
properties: [layer]
544554

555+
# supersede existing osm buildings with data from landmark geojson layer
556+
- fn: vectordatasource.transform.overlap
557+
params:
558+
base_layer: buildings
559+
cutting_layer: landmarks
560+
attribute: supersede
561+
target_attribute: superseded_by
562+
linear: false
563+
564+
# turn the landmark geojson polygons into point features
565+
- fn: vectordatasource.transform.mutate
566+
params:
567+
layer: landmarks
568+
start_zoom: 13
569+
geometry_expression: Point({properties}['origin'])
570+
properties_expression: "dict(filter(lambda p: p[0] not in ['supersede', 'origin'], {properties}.items()))"
571+
545572
# cut with admin_areas to put country_code attributes on roads
546573
# which are mostly within a particular country.
547574
- fn: vectordatasource.transform.overlap
@@ -1865,6 +1892,7 @@ post_process:
18651892
- addr_housenumber
18661893
- addr_street
18671894
- osm_relation
1895+
exclude: ['superseded_by']
18681896
# NOTE: max_merged_features is set to keep the time taken for geometry
18691897
# merging down, as it seems to go up with the square of the number of
18701898
# features.

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ future==0.16.0
77
gunicorn==19.9.0
88
hanzidentifier==1.0.2
99
hiredis==0.2.0
10+
kdtree
1011
lxml==4.6.2
1112
mapbox-vector-tile==1.2.0
1213
ModestMaps==1.4.7
1314
protobuf==3.4.0
1415
psycopg2==2.7.3.2
1516
pyclipper==1.0.6
1617
pycountry==17.9.23
18+
pyshp==2.3.0
1719
python-dateutil==2.6.1
1820
PyYAML==4.2b4
1921
git+https://github.com/tilezen/raw_tiles@master#egg=raw_tiles

vectordatasource/transform.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- encoding: utf-8 -*-
22
# transformation functions to apply to features
3+
import copy
34
import csv
45
import re
56
from collections import defaultdict
@@ -9758,3 +9759,44 @@ def override_with_ne_names(shape, props, fid, zoom):
97589759
props['name:' + language] = ne_name
97599760

97609761
return shape, props, fid
9762+
9763+
9764+
def mutate(ctx):
9765+
"""
9766+
We take a layer and we modify the geometry using the python expression in the query. The expressions have access to
9767+
both the existing shape and existing properties via python string format replacements {shape} and {properties}
9768+
respectively. Each expression is then eval'd to replace the existing feature in the layer with the result of the
9769+
expressions. By default the expressions are a no-op
9770+
"""
9771+
9772+
layer = ctx.params.get('layer')
9773+
assert layer, 'regenerate_geometry: missing layer'
9774+
geometry_expression = ctx.params.get('geometry_expression', '{shape}')
9775+
properties_expression = ctx.params.get('properties_expression', '{properties}')
9776+
assert geometry_expression or properties_expression, \
9777+
'mutate: requires at least one geometry or properties expression'
9778+
geometry_expression = geometry_expression.format(shape='shape', properties='props')
9779+
properties_expression = properties_expression.format(shape='shape', properties='props')
9780+
9781+
zoom = ctx.nominal_zoom
9782+
start_zoom = ctx.params.get('start_zoom', 0)
9783+
end_zoom = ctx.params.get('end_zoom')
9784+
if zoom < start_zoom:
9785+
return None
9786+
if end_zoom is not None and zoom >= end_zoom:
9787+
return None
9788+
9789+
# for the max zoom a transform needs to be re-entrant so we take a copy here
9790+
layer = copy.deepcopy(_find_layer(ctx.feature_layers, layer))
9791+
if layer is None:
9792+
return None
9793+
9794+
new_features = []
9795+
for feature in layer['features']:
9796+
shape, props, fid = feature
9797+
shape = eval(geometry_expression)
9798+
props = eval(properties_expression)
9799+
new_features.append((shape, props, fid))
9800+
9801+
layer['features'] = new_features
9802+
return layer

0 commit comments

Comments
 (0)