diff --git a/docs/layers.md b/docs/layers.md index e37879a9a..569f13156 100644 --- a/docs/layers.md +++ b/docs/layers.md @@ -151,7 +151,7 @@ Combination of OpenStreetMap administrative boundaries (zoom >= 8) and Natural E Polygons from OpenStreetMap representing building footprint, building label_placement points, building_part features, and address points. Starts at zoom 13 by including huge buildings, progressively adding all buildings at zoom 16+. Address points are available at zoom 16+, but marked with `min_zoom: 17` to suggest that they are suitable for display at zoom level 17 and higher. -Individual `building:part` geometries from OSM following the [Simple 3D Buildings](http://wiki.openstreetmap.org/wiki/Simple_3D_Buildings) tags at higher zoom levels are now exported as `building_part` features with specified `kind_detail`. +Individual `building:part` geometries from OSM following the [Simple 3D Buildings](http://wiki.openstreetmap.org/wiki/Simple_3D_Buildings) tags at higher zoom levels are now exported as `building_part` features with specified `kind_detail`. Building parts may receive a `root_id` corresponding to the building feature, if any, with which they intersect. Mapzen calculates the `landuse_kind` value by intercutting `buildings` with the `landuse` layer to determine if a building is over a parks, hospitals, universities or other landuse features. Use this property to modify the visual appearance of buildings over these features. For instance, light grey buildings look great in general, but aren't legible over most landuse colors unless they are darkened (or colorized to match landuse styling). @@ -480,7 +480,7 @@ To resolve inconsistency in data tagging in OpenStreetMap we normalize several o * `light_rail_routes` a list of light rail or rapid-transit passenger train routes. * `tram_routes` a list of tram routes. * `is_*` a set of boolean flags indicating whether this station has any routes of the given type. These are: `is_train`, `is_subway`, `is_light_rail`, `is_tram`, corresponding to the above `*_routes`. This is provided as a convenience for styling. -* `root_relation_id` an integer ID (of an OSM relation) which can be used to link or group together features which are related by being part of a larger feature. A full explanation of [relations](http://wiki.openstreetmap.org/wiki/Relation) wouldn't fit here, but the general idea is that all the station features which are part of the same [site](http://wiki.openstreetmap.org/wiki/Relation:site), [stop area](http://wiki.openstreetmap.org/wiki/Tag:public_transport%3Dstop_area) or [stop area group](http://wiki.openstreetmap.org/wiki/Relation:public_transport) should have the same ID to show they're related. Note that this information is only present on some stations. +* `root_id` an integer ID (of an OSM relation) which can be used to link or group together features which are related by being part of a larger feature. A full explanation of [relations](http://wiki.openstreetmap.org/wiki/Relation) wouldn't fit here, but the general idea is that all the station features which are part of the same [site](http://wiki.openstreetmap.org/wiki/Relation:site), [stop area](http://wiki.openstreetmap.org/wiki/Tag:public_transport%3Dstop_area) or [stop area group](http://wiki.openstreetmap.org/wiki/Relation:public_transport) should have the same ID to show they're related. Note that this information is only present on some stations. #### POI properties (only on `kind:bicycle_rental_station`): diff --git a/integration-test/653-unify-building-part.py b/integration-test/653-unify-building-part.py new file mode 100644 index 000000000..b3f7ba07c --- /dev/null +++ b/integration-test/653-unify-building-part.py @@ -0,0 +1,41 @@ +# http://www.openstreetmap.org/way/264768910 +# Way: One Madison +assert_has_feature( + 16, 19298, 24633, 'buildings', + { 'id': 264768910, 'kind': 'building', 'root_id': type(None) }) + +# http://www.openstreetmap.org/way/160967738 +assert_has_feature( + 16, 19298, 24633, 'buildings', + { 'id': 160967738, 'kind': 'building_part', 'root_id': 264768910 }) + +#http://www.openstreetmap.org/way/160967739 +assert_has_feature( + 16, 19298, 24633, 'buildings', + { 'id': 160967739, 'kind': 'building_part', 'root_id': 264768910 }) + + +# http://www.openstreetmap.org/relation/6062613 +# Relation: Ferry Building +assert_has_feature( + 16, 10486, 25326, 'buildings', + { 'id': 24460886, 'kind': 'building', 'root_id': type(None) }) + +# http://www.openstreetmap.org/way/404449724 +assert_has_feature( + 16, 10486, 25326, 'buildings', + { 'id': 404449724, 'kind': 'building_part', 'root_id': 24460886 }) + + +# http://www.openstreetmap.org/relation/1242762 +# Relation: Waterloo (tube and rail) +# http://www.openstreetmap.org/relation/238793 +# Relation: Waterloo (tube station) +# http://www.openstreetmap.org/relation/238792 +# Relation: London Waterloo +assert_has_feature( + 16, 32747, 21793, 'pois', + { 'id': 3638795617, 'root_id': 1242762, 'root_relation_id': type(None) }) +assert_has_feature( + 16, 32747, 21793, 'pois', + { 'id': 3638795618, 'root_id': 1242762, 'root_relation_id': type(None) }) diff --git a/queries.yaml b/queries.yaml index 7ea56273b..9890b47b2 100644 --- a/queries.yaml +++ b/queries.yaml @@ -651,11 +651,10 @@ post_process: params: source_layer: buildings start_zoom: 13 - end_zoom: 15 + end_zoom: 14 quantize: 13: vectordatasource.transform.quantize_height_round_nearest_10_meters 14: vectordatasource.transform.quantize_height_round_nearest_5_meters - 15: vectordatasource.transform.quantize_height_round_nearest_meter drop: - name - addr_housenumber @@ -663,6 +662,7 @@ post_process: - fn: vectordatasource.transform.simplify_and_clip params: {simplify_before: 16} + - fn: vectordatasource.transform.intercut params: base_layer: roads @@ -670,8 +670,14 @@ post_process: attribute: kind target_attribute: landuse_kind cutting_attrs: { sort_key: 'sort_key', reverse: True } + - fn: vectordatasource.transform.merge_features params: source_layer: roads start_zoom: 8 end_zoom: 15 + + - fn: vectordatasource.transform.buildings_unify + params: + source_layer: buildings + start_zoom: 15 diff --git a/test/test_transform.py b/test/test_transform.py index 7ed4f6749..a51deb0bc 100644 --- a/test/test_transform.py +++ b/test/test_transform.py @@ -213,3 +213,94 @@ def test_geometry_type(self): self.assertIsNotNone(sort_key_result) _, sort_key = sort_key_result self.assertEquals(int(sort_key), 223) + + +class BuildingsUnifyTest(unittest.TestCase): + + def _call_fut(self, building_shapes, building_part_shapes): + from tilequeue.tile import deserialize_coord + from tilequeue.process import Context + + building_features = [] + building_id = 1 + for building_shape in building_shapes: + building_props = dict( + id=building_id, + kind='building', + ) + building_feature = building_shape, building_props, building_id + building_features.append(building_feature) + building_id += 1 + + part_features = [] + building_part_id = building_id + for part_shape in building_part_shapes: + part_props = dict( + id=building_part_id, + kind='building_part', + ) + part_feature = part_shape, part_props, building_part_id + part_features.append(part_feature) + building_part_id += 1 + + building_features = building_features + part_features + building_feature_layer = dict( + features=building_features, + layer_datum=dict(name='buildings'), + ) + feature_layers = [building_feature_layer] + + ctx = Context( + feature_layers=feature_layers, + tile_coord=deserialize_coord('0/0/0'), + unpadded_bounds=None, + params=dict(source_layer='buildings'), + resources=None) + from vectordatasource.transform import buildings_unify + buildings_unify(ctx) + return building_feature_layer['features'] + + def test_no_overlap(self): + import shapely.geometry + building_shape = shapely.geometry.Polygon( + [(1, 1), (2, 2), (1, 2), (1, 1)]) + part_shape = shapely.geometry.Polygon( + [(10, 10), (20, 20), (10, 20), (10, 10)]) + result = self._call_fut([building_shape], [part_shape]) + building, part = result + part_props = part[1] + assert 'root_id' not in part_props + + def test_overlap(self): + import shapely.geometry + building_shape = shapely.geometry.Polygon( + [(1, 1), (20, 20), (10, 20), (1, 1)]) + part_shape = shapely.geometry.Polygon( + [(10, 10), (20, 20), (10, 20), (10, 10)]) + result = self._call_fut([building_shape], [part_shape]) + building, part = result + part_props = part[1] + root_id = part_props.get('root_id') + self.assertEquals(root_id, 1) + + def test_best_overlap(self): + import shapely.geometry + building1_shape = shapely.geometry.Polygon( + [(2, 1), (2, 2), (0, 2), (2, 1)]) + building2_shape = shapely.geometry.Polygon( + [(1, 1), (20, 20), (10, 20), (1, 1)]) + building3_shape = shapely.geometry.Polygon( + [(19, 1), (30, 30), (19, 30), (19, 1)]) + part_shape = shapely.geometry.Polygon( + [(10, 10), (20, 20), (10, 20), (10, 10)]) + + building_shapes = [building1_shape, building2_shape, building3_shape] + part_shapes = [part_shape] + result = self._call_fut(building_shapes, part_shapes) + for feature in result: + props = feature[1] + if props['kind'] == 'building_part': + root_id = props.get('root_id') + self.assertEquals(root_id, 2) + else: + assert 'root_id' not in props diff --git a/vectordatasource/transform.py b/vectordatasource/transform.py index 5ecd75ba3..c36774328 100644 --- a/vectordatasource/transform.py +++ b/vectordatasource/transform.py @@ -2441,7 +2441,7 @@ def normalize_station_properties(ctx): # that as a way for the client to link together related # features. if root_relation_id: - props['root_relation_id'] = root_relation_id + props['root_id'] = root_relation_id return layer @@ -3825,3 +3825,66 @@ def network_key(t): properties['all_shield_texts'] = [n[2] for n in networks] return (shape, properties, fid) + + +def buildings_unify(ctx): + """ + Unify buildings with their parts. Building parts will receive a + root_id property which will be the id of building parent they are + associated with. + """ + zoom = ctx.tile_coord.zoom + start_zoom = ctx.params.get('start_zoom', 0) + + if zoom < start_zoom: + return None + + source_layer = ctx.params.get('source_layer') + assert source_layer is not None, 'unify_buildings: missing source_layer' + feature_layers = ctx.feature_layers + layer = _find_layer(feature_layers, source_layer) + if layer is None: + return None + + class geom_with_building_id(object): + def __init__(self, geom, building_id): + self.geom = geom + self.building_id = building_id + self._geom = geom._geom + self.is_empty = geom.is_empty + + indexable_buildings = [] + parts = [] + for feature in layer['features']: + shape, props, feature_id = feature + kind = props.get('kind') + if kind == 'building': + building_id = props.get('id') + if building_id: + indexed_building = geom_with_building_id(shape, building_id) + indexable_buildings.append(indexed_building) + elif kind == 'building_part': + parts.append(feature) + + if not (indexable_buildings and parts): + return + + buildings_index = STRtree(indexable_buildings) + + for part in parts: + best_overlap = 0 + root_building_id = None + + part_shape, part_props, part_feature_id = part + + indexed_buildings = buildings_index.query(part_shape) + for indexed_building in indexed_buildings: + building_shape = indexed_building.geom + intersection = part_shape.intersection(building_shape) + overlap = intersection.area + if overlap > best_overlap: + best_overlap = overlap + root_building_id = indexed_building.building_id + + if root_building_id is not None: + part_props['root_id'] = root_building_id