Skip to content

Commit

Permalink
Merge pull request #1703 from tilezen/zerebubuth/1227-improve-road-me…
Browse files Browse the repository at this point in the history
…rging

Improve road merging
  • Loading branch information
zerebubuth authored Nov 14, 2018
2 parents 0553ea5 + 3052728 commit d4f15bf
Show file tree
Hide file tree
Showing 5 changed files with 555 additions and 11 deletions.
56 changes: 56 additions & 0 deletions integration-test/1227-improve-road-merging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# -*- encoding: utf-8 -*-
from . import FixtureTest


class MergeJunctionTest(FixtureTest):

def test_junction(self):
from tilequeue.tile import coord_to_bounds
from shapely.geometry import LineString, asShape
from ModestMaps.Core import Coordinate
import dsl

z, x, y = (12, 2048, 2048)

minx, miny, maxx, maxy = coord_to_bounds(
Coordinate(zoom=z, column=x, row=y))
midx = 0.5 * (minx + maxx)
midy = 0.5 * (miny + maxy)

road_props = dict(
highway='residential',
source='openstreetmap.org',
)

# make a tile with 4 roads in an X shape, as below.
#
# \ /
# 1 2
# \/
# /\
# 3 4
# / \
#
# these should get merged into two lines 1->4 & 2->3.
self.generate_fixtures(
dsl.way(1, LineString([[minx, maxy], [midx, midy]]), road_props),
dsl.way(2, LineString([[maxx, maxy], [midx, midy]]), road_props),
dsl.way(3, LineString([[minx, miny], [midx, midy]]), road_props),
dsl.way(4, LineString([[maxx, miny], [midx, midy]]), road_props),
)

with self.features_in_tile_layer(z, x, y, 'roads') as features:
# should have merged into a single _feature_
self.assertTrue(len(features) == 1)

# when the test suite runs in "download only mode", an empty
# set of features is passed into this block. the assertion
# is shorted out, so we need this additional check which is
# trivially satisfied in the case we're doing real testing.
if len(features) == 1:
# the shape should be a multilinestring
shape = asShape(features[0]['geometry'])
self.assertTrue(shape.geom_type == 'MultiLineString')

# with two internal linestrings
self.assertTrue(len(shape.geoms) == 2)
24 changes: 17 additions & 7 deletions integration-test/358-merge-same-roads.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,30 @@ def _query_highway(highway):
class MergeSameRoads(FixtureTest):

def test_roads_merged(self):
from collections import defaultdict
from shapely.geometry import asShape

# count the unique parameters - there should only be one, indicating
# that the roads have been merged.
self.load_fixtures(
[_query_highway(t) for t in ('motorway', 'primary', 'trunk')],
clip=self.tile_bbox(8, 41, 99))

with self.features_in_tile_layer(8, 41, 99, 'roads') as roads:
features = set()
features = defaultdict(list)

for road in roads:
props = frozenset(_freeze(road['properties']))
self.assertFalse(
props in features,
'Duplicate properties %r in roads layer, but '
'properties should be unique.'
% road['properties'])
features.add(props)
geom = asShape(road['geometry'])

for f in features[props]:
if f.disjoint(geom):
self.assertTrue(
False,
'Duplicate properties %r in roads layer for '
'disjoint geometries (%r & %r), but '
'properties should be unique or geometries '
'intersecting.' % (road['properties'], f.wkt,
geom.wkt))

features[props].append(geom)
30 changes: 29 additions & 1 deletion queries.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1129,19 +1129,47 @@ post_process:
start_zoom: 4
end_zoom: 13

# first, merge linestrings together where the properties are the same, and
# make sure we merge across junctions. this will create a set of non-simple
# multilinestrings which do not have nodes at junctions.
#
# note that the linestrings are already simplified by preceding simplify
# and clip stage.
- fn: vectordatasource.transform.merge_line_features
params:
source_layer: roads
start_zoom: 8
end_zoom: 16

# setting the following will try to merge linestrings across junctions
# (i.e: more than 2 roads meeting at a point) where the angle between
# roads at that point is less than 5 degrees.
merge_junctions: true
merge_junction_angle: 5.0

# simplify roads again, to take advantage of any opportunities opened up
# by merging roads with the same properties in the previous step.
- fn: vectordatasource.transform.simplify_layer
params:
source_layer: roads
start_zoom: 8
end_zoom: 16
tolerance: 1.0

# merge _again_, this time to merge any features which no longer intersect
# because of the simplification step and can be packed into the same
# MultiLineString. also, drop short segments within the MultiLineString
# which didn't get merged into something larger - at 0.1 pixels, these
# probably aren't visible anyway.
- fn: vectordatasource.transform.merge_line_features
params:
source_layer: roads
start_zoom: 8
end_zoom: 16
# setting the following will cause lines, or parts of multi-lines,
# shorter than 0.1px at nominal zoom to be dropped.
drop_short_segments: true
drop_length_pixels: 0.1

# NOTE: want to do this _before_ buildings_unify, as after that we might not
# have a feature ID to match buildings on!
- fn: vectordatasource.transform.backfill_from_other_layer
Expand Down
158 changes: 158 additions & 0 deletions test/test_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -785,3 +785,161 @@ def test_guess_network_br(self):
self.assertEqual([], _guess_network_br({}))
# should be empty for a blank ref
self.assertEqual([], _guess_network_br(dict(ref="")))


# utility method to sort linestrings canonically, so that they can
# be compared equal in a list. this allows us to use assertEqual on
# multilinestrings where we don't care about the order of the lines
# in the multi.
def _sort_linestrings(lines):
return list(sorted(lines, key=lambda l: l.wkt))


class MergeJunctionTest(unittest.TestCase):

def test_simple_merge(self):
from shapely.geometry import LineString, MultiLineString
from vectordatasource.transform import \
_merge_junctions_in_multilinestring

angle_tolerance = 15.0
mls = MultiLineString([
LineString([[0, 0], [1, 0]]),
LineString([[-1, 0], [0, 0]]),
])

shape = _merge_junctions_in_multilinestring(mls, angle_tolerance)

expected = LineString([[-1, 0], [0, 0], [1, 0]])
self.assertEquals(shape, expected)

def test_four_way_merge(self):
from shapely.geometry import LineString, MultiLineString
from vectordatasource.transform import \
_merge_junctions_in_multilinestring

angle_tolerance = 15.0
mls = MultiLineString([
LineString([[0, 0], [1, 0]]),
LineString([[-1, 0], [0, 0]]),
LineString([[0, 0], [0, 1]]),
LineString([[0, 0], [0, -1]]),
])

shape = _merge_junctions_in_multilinestring(mls, angle_tolerance)

expected = MultiLineString([
LineString([[-1, 0], [0, 0], [1, 0]]),
LineString([[0, -1], [0, 0], [0, 1]]),
])

self.assertEquals(shape.geom_type, expected.geom_type)
self.assertEquals(_sort_linestrings(shape.geoms),
_sort_linestrings(expected.geoms))

def test_merge_tolerance(self):
from shapely.geometry import LineString, MultiLineString
from vectordatasource.transform import \
_merge_junctions_in_multilinestring

angle_tolerance = 0.0
# these have been adjusted so that none of them meet at
# exact angles, so no merge should take place.
mls = MultiLineString([
LineString([[0, 0], [1, 0.1]]),
LineString([[-1, 0], [0, 0]]),
LineString([[0, 0], [0.1, 1]]),
LineString([[0, 0], [0, -1]]),
])

shape = _merge_junctions_in_multilinestring(mls, angle_tolerance)

expected = mls
self.assertEquals(shape.geom_type, expected.geom_type)
self.assertEquals(_sort_linestrings(shape.geoms),
_sort_linestrings(expected.geoms))

def test_partition_mls_nonoverlapping(self):
from shapely.geometry import LineString, MultiLineString
from vectordatasource.transform import \
_linestring_nonoverlapping_partition

# these are already non-overlapping, so should not be split
mls = MultiLineString([
LineString([[0, 0], [1, 0]]),
LineString([[0, 1], [1, 1]]),
])

shapes = _linestring_nonoverlapping_partition(mls)

self.assertEquals(shapes, [mls])

def test_partition_mls_simple_overlapping(self):
from shapely.geometry import LineString, MultiLineString
from vectordatasource.transform import \
_linestring_nonoverlapping_partition

ls1 = LineString([[-1, 0], [1, 0]])
ls2 = LineString([[0, -1], [0, 1]])

# these are overlapping, so should be split
mls = MultiLineString([ls1, ls2])

shapes = _linestring_nonoverlapping_partition(mls)

self.assertEquals(shapes, [ls1, ls2])

def test_partition_mls_overlapping(self):
from shapely.geometry import LineString, MultiLineString
from vectordatasource.transform import \
_linestring_nonoverlapping_partition

ls1 = LineString([[-3, 0], [3, 0]])
ls2 = LineString([[0, -3], [0, 3]])
ls3 = LineString([[-3, 1], [3, 1]])
ls4 = LineString([[1, -3], [1, 3]])

# these are overlapping, so should be split
mls = MultiLineString([ls1, ls2, ls3, ls4])

shapes = _linestring_nonoverlapping_partition(mls)

self.assertEquals(shapes, [
MultiLineString([ls1, ls3]),
MultiLineString([ls2, ls4]),
])


class TestBoundingBoxIntersection(unittest.TestCase):

def _intersects(self, a, b):
from vectordatasource.transform import _intersects_bounds
self.assertTrue(_intersects_bounds(a, b))

def _disjoint(self, a, b):
from vectordatasource.transform import _intersects_bounds
self.assertFalse(_intersects_bounds(a, b))

def test_left(self):
self._disjoint((0, 0, 1, 1), (2, 0, 3, 1))

def test_right(self):
self._disjoint((2, 0, 3, 1), (0, 0, 1, 1))

def test_top(self):
self._disjoint((0, 0, 1, 1), (0, 2, 1, 3))

def test_bottom(self):
self._disjoint((0, 2, 1, 3), (0, 0, 1, 1))

def test_contains(self):
self._intersects((0, 0, 5, 5), (1, 1, 4, 4))

def tests_contained(self):
self._intersects((1, 1, 4, 4), (0, 0, 5, 5))

def test_half_left(self):
self._intersects((0, 0, 5, 2), (1, 1, 4, 3))

def test_half_top(self):
self._intersects((0, 0, 2, 5), (1, 1, 3, 4))
Loading

0 comments on commit d4f15bf

Please sign in to comment.