Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions integration-test/1732-restore-building-scale-rank.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# -*- encoding: utf-8 -*-
from . import FixtureTest


class Buildings(object):
def __init__(self, z, x, y, landuse_kind=None):
from tilequeue.tile import coord_to_mercator_bounds
from ModestMaps.Core import Coordinate
import dsl

bounds = coord_to_mercator_bounds(Coordinate(zoom=z, column=x, row=y))
self.origin_x = bounds[0]
self.origin_y = bounds[1]
self.buildings = []
self.way_id = 1

if landuse_kind is not None:
self.buildings.append(dsl.way(self.way_id, dsl.tile_box(z, x, y), {
'landuse': landuse_kind,
'source': 'openstreetmap.org',
}))
self.way_id += 1

# minx, miny, maxx, maxy are in mercator meters from the bottom left
# corner of the tile, so it's easier to calculate area.
def add(self, minx, miny, maxx, maxy, height=5, extra_tags={}):
from dsl import way
from shapely.geometry import box
from shapely.ops import transform
from tilequeue.tile import reproject_mercator_to_lnglat

shape = box(self.origin_x + minx, self.origin_y + miny,
self.origin_x + maxx, self.origin_y + maxy)
shape_lnglat = transform(reproject_mercator_to_lnglat, shape)

tags = {
'source': 'openstreetmap.org',
'building': 'yes',
}
tags.update(extra_tags)

self.buildings.append(way(self.way_id, shape_lnglat, tags))
self.way_id += 1


class BuildingScaleRankTest(FixtureTest):

def _setup_row(self, z, x, y, width, depth, n, height=5, extra_tags={},
landuse_kind=None):
b = Buildings(z, x, y, landuse_kind=landuse_kind)
for i in xrange(0, n * width, width):
b.add(i, 0, i + width, depth, height=height, extra_tags=extra_tags)

self.generate_fixtures(*b.buildings)

def test_not_merged_z16(self):
# make a row of buildings, check that they get assigned scale
# rank and are _not_ merged at z16.
z, x, y = (16, 0, 0)

self._setup_row(z, x, y, 10, 40, 10)

# and there are 10 buildings with scale rank 5
self.assert_n_matching_features(
z, x, y, 'buildings', {
'kind': 'building',
'scale_rank': 5,
}, 10)

# and none with any other scale rank value.
self.assert_no_matching_feature(
z, x, y, 'buildings', {
'scale_rank': (lambda r: r != 5)
})

def test_merged_z15(self):
# make a row of buildings, check that they get assigned scale
# rank and are merged at z15.
z, x, y = (15, 0, 0)

self._setup_row(z, x, y, 10, 40, 10)

# should be only 1 building feature now
self.assert_n_matching_features(
z, x, y, 'buildings', {
'kind': 'building',
'scale_rank': 5,
}, 1)

# and none with any other scale rank value.
self.assert_no_matching_feature(
z, x, y, 'buildings', {
'scale_rank': (lambda r: r != 5)
})

def test_drop_scale_rank_5_at_z14(self):
# make a row of buildings, check that they get assigned scale
# rank and are merged at z15.
z, x, y = (14, 0, 0)

self._setup_row(z, x, y, 10, 40, 10)

# scale_rank 5 features should be dropped at this zoom.
self.assert_no_matching_feature(
z, x, y, 'buildings', {
'kind': 'building',
'scale_rank': 5,
})

def test_merged_z14(self):
# make a row of buildings, check that they get assigned scale
# rank and are merged at z14.
z, x, y = (14, 0, 0)

self._setup_row(z, x, y, 10, 50, 10, landuse_kind='retail')

# should be only 1 building feature now
self.assert_n_matching_features(
z, x, y, 'buildings', {
'kind': 'building',
'scale_rank': 4,
}, 1)

# and none with any other scale rank value.
self.assert_no_matching_feature(
z, x, y, 'buildings', {
'scale_rank': (lambda r: r != 4)
})

def test_tiny_shed_z17(self):
# check that a tiny shed, assigned zoom 17 in the buildings.yaml,
# doesn't get re-assigned a lower zoom. it should stay at z17.
z, x, y = (16, 0, 0)

# make one 1x1m building.
self._setup_row(z, x, y, 1, 1, 1)

self.assert_n_matching_features(
z, x, y, 'buildings', {
'kind': 'building',
'min_zoom': 17,
}, 1)

def test_slightly_larger_outbuilding_z16(self):
# check that a small building (or large shed), assigned zoom 16 in the
# buildings.yaml, doesn't get re-assigned a lower zoom. it should stay
# at z16.
z, x, y = (16, 0, 0)

# make one 4x4m building.
self._setup_row(z, x, y, 4, 4, 1)

self.assert_n_matching_features(
z, x, y, 'buildings', {
'kind': 'building',
'min_zoom': 16,
}, 1)
53 changes: 40 additions & 13 deletions queries.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1254,7 +1254,7 @@ post_process:
source_layers: [buildings]
pixel_area: 0.25

- fn: vectordatasource.transform.merge_building_features
- fn: vectordatasource.transform.quantize_height
params:
source_layer: buildings
start_zoom: 13
Expand All @@ -1263,6 +1263,44 @@ post_process:
13: vectordatasource.transform.quantize_height_round_nearest_20_meters
14: vectordatasource.transform.quantize_height_round_nearest_10_meters
15: vectordatasource.transform.quantize_height_round_nearest_10_meters

# assign `scale_rank` - BEFORE merging, as we want to assign this based on
# the original area/height/volume of the building before it is possibly
# merged with others. clients might use scale_rank for filtering, in which
# case they want to see the original value.
- fn: vectordatasource.transform.csv_match_properties
resources:
matcher:
type: file
init_fn: vectordatasource.transform.CSVMatcher
path: spreadsheets/scale_rank/buildings.csv
params:
source_layer: buildings
target_value_type: int

# fit min zoom onto the scale rank, as we'll be using the scale rank rather
# than the min zoom to calculate whether a building should make it into a
# tile. having them split separately means we can't merge them together.
#
# note: this only _increases_ the min_zoom to be inline with the min that
# matches the scale_rank, it will never _decrease_ the min_zoom. i.e: it will
# push a z14 building with scale_rank=5 down to z15, but won't lift a z17
# building up, no matter what its scale_rank.
- fn: vectordatasource.transform.clamp_min_zoom
Copy link
Copy Markdown
Member

@nvkelso nvkelso Dec 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens in zoom 16? If we clamp feature inclusion at zooms 13, 14, 15 based on scale_rank, but don't clamp at max zoom 16, it seems like zoom 16 would include new features that could have a min_zoom < 16 but weren't shown at the earlier zooms?

Should we go farther and make scale_rank and min_zoom consistent (and then mark scale_rank for retirement in v2)?

From the map examples, including the clamping:

scale_rank min_zoom color in map
1 13 dark green
2 13.5 ¿missing?
3 14 dark blue
4 14.5 light blue
5 15 light green

In the existing building.yaml file:

  - &z13_area_volume
      all:
        any:
          way_area: { min: 5000 }
          volume: { min: 150000 }
        not: { "tags->location": "underground" }
  - &z14_area_volume
      any:
        way_area: { min: 3000 }
        volume: { min: 100000 }
  - &z15_area_volume
      way_area: { min: 20 }
  - &z16_area_volume
      way_area: { min: 10 }

In Bubble Wrap:
problematic at zoom 15

            filter:
                # show footprints for buildings at least one zoom level before they will be extruded
                - { $zoom: 13, scale_rank: [1,2] }
                - { $zoom: 14, scale_rank: [1,2,3] }
                - { $zoom: 15, height: { min: 100 } }
                - { $zoom: 15, area: { min: 500 } }
                - { $zoom: 15, volume: { min: 100000 } }
                - { $zoom: 16, area: { min: 100 } }
                - { $zoom: 16, volume: { min: 50000 } }
                - { $zoom: { min: 17 }, area: true }

In Refill (map):
problematic at zoom 15

        footprints:
            filter:
                any:
                    - { $zoom: [13], scale_rank: [1,2] }
                    - { $zoom: [14], scale_rank: [1,2,3] }
                    - { $zoom: [15], height: { min: 100 } }
                    - { $zoom: [15], area: { min: 500 } }
                    - { $zoom: [15], volume: { min: 100000 } }
                    - { $zoom: [16], area: { min: 100 } }
                    - { $zoom: [16], volume: { min: 50000 } }
                    - { $zoom: { min: 17 }, area: true }

Then the existing scale_rank.csv is:

area::int,height::float,volume::int,landuse_kind,scale_rank
>=100000,*,*,*,1
*,>=250,*,*,1
*,*,>=300000,*,1
>=20000,*,*,*,2
>=5000,*,*,+,2
*,>=150,*,*,2
*,*,>=150000,*,2
>=5000,*,*,*,3
>=3000,*,*,+,3
*,>=100,*,*,3
*,*,>=100000,*,3
>=1000,*,*,*,4
>=500,*,*,+,4
*,*,>=50000,*,4
+,*,*,*,5

Should the building.yaml file change like (I'm fussy on the landuse_kind and 14.5 logic)?

  - &z13_area_volume
      all:
        any:
          way_area: { min: 100000 } # was 5000, larger value from scale_rank CSV
          height: { min: 250 }            # newly added from scale_rank CSV
          volume: { min: 300000 }   # was 150000, larger value from scale_rank CSV
        not: { "tags->location": "underground" }
  - &z14_area_volume
      any:
        way_area: { min: 3000 }     # no change, same as scale_rank CSV (prefer landuse area)
        height: { min: 100 }            # newly added from scale_rank CSV
        volume: { min: 100000 }    # no change, same as scale_rank CSV
       not: { "tags->location": "underground" }  # NEW
# needs corresponding YAML change later on for 14.5 min_zoom assignment
  - &z14d5_area_volume
      any:
        way_area: { min: 500 }     # NEW, same as scale_rank CSV (prefer landuse area)
        height: { min: 100 }          # NEW, same as scale_rank CSV (from z14_area_volume)
        volume: { min: 50000 }   # NEW, same as scale_rank CSV
       not: { "tags->location": "underground" }  # NEW
  - &z15_area_volume
      any:
        way_area: { min: 200 }      # was 20, larger value between scale_rank CSV (100) and map style (500)
        height: { min: 15 }             # NEW, middle ground to remove < 3 story buildings; map style 100
        volume: { min: 10000 }    # NEW, smaller compromise between area * height, map style (100000)
  - &z16_area_volume
      way_area: { min: 10 }          # no change, same as scale_rank CSV, because of backyard sheds
# Implied that features with way_area smaller than 10 are `min_zoom: 17` later in YAML

Then this clamp section wouldn't be needed at all? We leave the scale_rank CSV alone, but only modify the min_zoom calcs?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens in zoom 16?

Ah, well spotted, thanks! Yes, we should make that consistent. I've made the change in 449a1d7.

Should we go farther and make scale_rank and min_zoom consistent (and then mark scale_rank for retirement in v2)? ... Then this clamp section wouldn't be needed at all? We leave the scale_rank CSV alone, but only modify the min_zoom calcs?

Unfortunately, the scale_rank calculation makes use of the landuse_kind, which is assigned by a post-processor and not known when we assign min_zoom in the database. I'm all for simplifying, but we'll end up with a process that looks a lot like the current one (i.e: assign the minimum min_zoom for the feature, then assign scale_rank, then use scale_rank to clamp min_zoom - basically dropping half a zoom based on landuse_kind).

One question is how the min_zoom logic works in the client. If we assign a min_zoom of 14.5, will that be visible in the zoom 14 tile? Or would it require work-arounds in the style file (filter: function() { return $zoom > feature.min_zoom - 0.5; }) to make sure that we're not including a bunch of data in the tile that'll never be shown?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We talked about this IRL...

Now that the clamp is always applied I think we're aways consistent, thanks!

Please add tests to ensure min_zoom: 16 and min_zoom: 17 buildings (tiny ones!) still get their min_zoom preserved, then I think we're good for merging.

I filed #1745 to address removing scale_rank entirely (just in favor of min_zoom) for eventual v2 milestone.

params:
layer: buildings
start_zoom: 14
property: scale_rank
clamp:
5: 15
4: 14
3: 14

- fn: vectordatasource.transform.merge_building_features
params:
source_layer: buildings
start_zoom: 13
end_zoom: 16
# todo: keep instead of drop?
drop:
- name
Expand All @@ -1283,17 +1321,6 @@ post_process:
source_layers: [buildings]
pixel_area: 0.25

# assign `scale_rank` - AFTER merging, as this changes the area and volume
- fn: vectordatasource.transform.csv_match_properties
resources:
matcher:
type: file
init_fn: vectordatasource.transform.CSVMatcher
path: spreadsheets/scale_rank/buildings.csv
params:
source_layer: buildings
target_value_type: int

- fn: vectordatasource.transform.numeric_min_filter
params:
source_layer: buildings
Expand All @@ -1317,7 +1344,7 @@ post_process:
source_layer: buildings
start_zoom: 14
end_zoom: 15
where: scale_rank > 3
where: scale_rank > 4

- fn: vectordatasource.transform.drop_small_inners
params:
Expand Down
84 changes: 72 additions & 12 deletions vectordatasource/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -3452,6 +3452,42 @@ def _merge_features_by_property(
return new_features


def quantize_height(ctx):
"""
Quantize the height property of features in the layer according to the
per-zoom configured quantize function.
"""

params = _Params(ctx, 'quantize_height')
zoom = ctx.nominal_zoom
source_layer = params.required('source_layer')
start_zoom = params.optional('start_zoom', default=0, typ=int)
end_zoom = params.optional('end_zoom', typ=int)
quantize_cfg = params.required('quantize', typ=dict)

layer = _find_layer(ctx.feature_layers, source_layer)
if layer is None:
return None

if zoom < start_zoom:
return None
if end_zoom is not None and zoom >= end_zoom:
return None

quantize_fn_dotted_name = quantize_cfg.get(zoom)
if not quantize_fn_dotted_name:
# no changes at this zoom
return None

quantize_height_fn = resolve(quantize_fn_dotted_name)
for shape, props, fid in layer['features']:
height = props.get('height', None)
if height is not None:
props['height'] = quantize_height_fn(height)

return None


def merge_building_features(ctx):
zoom = ctx.nominal_zoom
source_layer = ctx.params.get('source_layer')
Expand All @@ -3476,13 +3512,6 @@ def merge_building_features(ctx):
# values which retain detail.
tolerance = min(5, 0.4 * tolerance_for_zoom(zoom))

quantize_height_fn = None
quantize_cfg = ctx.params.get('quantize')
if quantize_cfg:
quantize_fn_dotted_name = quantize_cfg.get(zoom)
if quantize_fn_dotted_name:
quantize_height_fn = resolve(quantize_fn_dotted_name)

def _props_pre((shape, props, fid)):
if exclusions:
for prop in exclusions:
Expand All @@ -3498,11 +3527,6 @@ def _props_pre((shape, props, fid)):
for prop in drop:
props.pop(prop, None)

if quantize_height_fn:
height = props.get('height', None)
if height is not None:
props['height'] = quantize_height_fn(height)

return props

def _props_post((merged_shape, props, fid)):
Expand Down Expand Up @@ -8261,3 +8285,39 @@ def backfill(ctx):
props[k] = v

return None


def clamp_min_zoom(ctx):
"""
Clamps the min zoom for features depending on context.
"""

params = _Params(ctx, 'clamp_min_zoom')
layer_name = params.required('layer')
start_zoom = params.optional('start_zoom', default=0, typ=int)
end_zoom = params.optional('end_zoom', typ=int)
clamp = params.required('clamp', typ=dict)
property_name = params.required('property')

# check that we're in the zoom range where this post-processor is supposed
# to operate.
if ctx.nominal_zoom < start_zoom:
return None
if end_zoom is not None and ctx.nominal_zoom >= end_zoom:
return None

layer = _find_layer(ctx.feature_layers, layer_name)

features = layer['features']
for feature in features:
_, props, _ = feature

value = props.get(property_name)
min_zoom = props.get('min_zoom')

if value is not None and min_zoom is not None:
min_val = clamp.get(value)
if min_val is not None and min_val > min_zoom:
props['min_zoom'] = min_val

return None
4 changes: 2 additions & 2 deletions yaml/buildings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ globals:
not: { "tags->location": "underground" }
- &z14_area_volume
any:
way_area: { min: 3000 }
volume: { min: 100000 }
way_area: { min: 500 }
volume: { min: 50000 }
- &z15_area_volume
way_area: { min: 20 }
- &z16_area_volume
Expand Down