Skip to content

Commit 0dc91f5

Browse files
authored
Add missing suburbs (#150)
* add missing suburbs in output * correctly parse zone type for suburbs * clearer parsing of zone types * tests: fix expected values * fix conversion to geo for GeometryCollection & build cache * update ubuntu version for tests to 22.04 (LTS) There appears to be an inconcistency with more recent geos libraries. * fix city duplicates * filter out empty polygons when building boundary This would lead to computing bbox of empty shapes, which leads to many warning while importing cosmogony's output, and some inconsistencies in the display. * compute neighbourhood's geometry relative to their parent They were previously mixed with suburbs, which led to weird or empty geometries.
1 parent 362c431 commit 0dc91f5

File tree

8 files changed

+152
-104
lines changed

8 files changed

+152
-104
lines changed

.github/workflows/test-and-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on: [push, pull_request]
44

55
jobs:
66
tests:
7-
runs-on: ubuntu-18.04
7+
runs-on: ubuntu-22.04
88
steps:
99

1010
- name: Checkout repository

Dockerfile

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ RUN apt-get update && apt-get install -y libgeos-c1v5 libgeos-dev && apt-get cle
77

88
COPY . ./
99

10-
RUN cargo build --profile production
10+
RUN --mount=type=cache,target=/usr/local/cargo/registry \
11+
--mount=type=cache,target=/srv/cosmogony/target \
12+
cargo build --profile production
13+
14+
RUN --mount=type=cache,target=/srv/cosmogony/target \
15+
cp target/production/cosmogony cosmogony.bin
1116

1217
FROM debian:buster-slim
1318

@@ -16,6 +21,6 @@ WORKDIR /srv
1621
ENV DEBIAN_FRONTEND noninteractive
1722
RUN apt-get update && apt-get install -y libgeos-c1v5 libgeos-dev && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
1823

19-
COPY --from=builder /srv/cosmogony/target/production/cosmogony /usr/bin/cosmogony
24+
COPY --from=builder /srv/cosmogony/cosmogony.bin /usr/bin/cosmogony
2025

2126
ENTRYPOINT ["cosmogony"]

cosmogony/src/read.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ fn read_zones(
99
) -> impl std::iter::Iterator<Item = Result<Zone, Error>> {
1010
reader
1111
.lines()
12-
.map(|l| l.map_err(|err| anyhow!("{}", err)))
12+
.map(|l| l.map_err(|err| err.into()))
1313
.map(|l| l.and_then(|l| serde_json::from_str(&l).map_err(|err| anyhow!("{}", err))))
1414
}
1515

cosmogony/src/zone.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,20 @@ impl ZoneType {
3535
ZoneType::NonAdministrative => "non_administrative",
3636
}
3737
}
38+
39+
pub fn parse(s: &str) -> Option<Self> {
40+
Some(match s {
41+
"suburb" | "quarter" | "neighbourhood" => Self::Suburb,
42+
"city_district" => Self::CityDistrict,
43+
"city" | "town" | "village" => Self::City,
44+
"state_district" => Self::StateDistrict,
45+
"state" => Self::State,
46+
"country_region" => Self::CountryRegion,
47+
"country" => Self::Country,
48+
"non_administrative" => Self::NonAdministrative,
49+
_ => return None,
50+
})
51+
}
3852
}
3953

4054
#[derive(Copy, Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]

src/additional_zones.rs

Lines changed: 107 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
use crate::hierarchy_builder::ZonesTree;
22
use crate::is_place;
3+
use crate::zone_ext::ZoneExt;
4+
use anyhow::{Context, Result};
35
use cosmogony::{Zone, ZoneIndex, ZoneType};
46
use geo::prelude::BoundingRect;
57
use geo_types::{Coordinate, MultiPolygon, Point, Rect};
68
use geos::{Geom, Geometry};
9+
use itertools::Itertools;
710
use osmpbfreader::{OsmId, OsmObj};
811
use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator};
912
use std::collections::BTreeMap;
1013

11-
use crate::zone_ext::ZoneExt;
12-
1314
fn difference<'a>(g: &geos::Geometry<'a>, other: &Zone) -> Option<geos::Geometry<'a>> {
1415
let zone_as_geos: Option<Geometry> = other.boundary.as_ref().and_then(|b| {
1516
b.try_into()
@@ -30,12 +31,13 @@ fn difference<'a>(g: &geos::Geometry<'a>, other: &Zone) -> Option<geos::Geometry
3031
}
3132
}
3233

33-
pub fn compute_additional_cities(
34+
pub fn compute_additional_places(
3435
zones: &mut Vec<Zone>,
3536
parsed_pbf: &BTreeMap<OsmId, OsmObj>,
3637
zones_rtree: ZonesTree,
3738
) {
3839
let place_zones = read_places(parsed_pbf);
40+
3941
info!(
4042
"there are {} places, we'll try to make boundaries for them",
4143
place_zones.len()
@@ -48,17 +50,20 @@ pub fn compute_additional_cities(
4850
get_parent(place, zones, &zones_rtree).map(|parent| (parent, place))
4951
})
5052
.filter(|(parent, place)| {
51-
parent
52-
.zone_type
53-
.as_ref()
54-
.map(|x| {
55-
if *x == ZoneType::Country {
53+
(parent.zone_type)
54+
.map(|parent_zone| {
55+
if parent_zone == ZoneType::Country {
5656
info!(
5757
"Ignoring place with id {} and country {} as parent",
5858
place.osm_id, parent.osm_id
5959
);
6060
}
61-
*x > ZoneType::City && *x < ZoneType::Country
61+
62+
// Ensuring zones are stricly increasing also ensures there will be no
63+
// duplicates, for example by adding an admin label which is inside its
64+
// boundary.
65+
parent_zone > place.zone_type.unwrap_or(parent_zone)
66+
&& parent_zone < ZoneType::Country
6267
})
6368
.unwrap_or(false)
6469
})
@@ -78,20 +83,19 @@ pub fn compute_additional_cities(
7883
candidate_parent_zones.len()
7984
);
8085

81-
let new_cities: Vec<Vec<Zone>> = {
86+
let new_cities: Vec<Zone> = {
8287
candidate_parent_zones
8388
.into_par_iter()
8489
.filter(|(_, places)| !places.is_empty())
8590
.map(|(parent, places)| compute_voronoi(parent, &places, zones, &zones_rtree))
91+
.flatten()
8692
.collect()
8793
};
88-
for cities in new_cities.into_iter() {
89-
publish_new_cities(zones, cities);
90-
}
94+
95+
publish_new_places(zones, new_cities);
9196
}
9297

9398
fn get_parent<'a>(place: &Zone, zones: &'a [Zone], zones_rtree: &ZonesTree) -> Option<&'a Zone> {
94-
use itertools::Itertools;
9599
zones_rtree
96100
.fetch_zone_bbox(place)
97101
.into_iter()
@@ -119,96 +123,114 @@ fn read_places(parsed_pbf: &BTreeMap<OsmId, OsmObj>) -> Vec<Zone> {
119123
if !is_place(obj) {
120124
return None;
121125
}
122-
if let OsmObj::Node(ref node) = obj {
123-
let next_index = ZoneIndex { index };
124-
if let Some(mut zone) = Zone::from_osm_node(node, next_index) {
125-
if zone.name.is_empty() {
126-
return None;
127-
}
128-
zone.zone_type = Some(ZoneType::City);
129-
zone.center = Some(Point::<f64>::new(node.lon(), node.lat()));
130-
zone.bbox = zone.center.as_ref().map(|p| {
131-
Rect::new(
132-
Coordinate {
133-
x: p.0.x - std::f64::EPSILON,
134-
y: p.0.y - std::f64::EPSILON,
135-
}, // min
136-
Coordinate {
137-
x: p.0.x + std::f64::EPSILON,
138-
y: p.0.y + std::f64::EPSILON,
139-
}, // max
140-
)
141-
});
142-
zone.is_generated = true;
143-
return Some(zone);
144-
}
126+
127+
let node = obj.node()?;
128+
let next_index = ZoneIndex { index };
129+
let mut zone = Zone::from_osm_node(node, next_index)?;
130+
131+
if zone.name.is_empty() {
132+
return None;
145133
}
146-
None
134+
135+
zone.center = Some(Point::<f64>::new(node.lon(), node.lat()));
136+
zone.bbox = zone.center.as_ref().map(|p| {
137+
Rect::new(
138+
Coordinate {
139+
x: p.0.x - std::f64::EPSILON,
140+
y: p.0.y - std::f64::EPSILON,
141+
}, // min
142+
Coordinate {
143+
x: p.0.x + std::f64::EPSILON,
144+
y: p.0.y + std::f64::EPSILON,
145+
}, // max
146+
)
147+
});
148+
149+
zone.is_generated = true;
150+
Some(zone)
147151
})
148152
.collect()
149153
}
150154

151-
fn convert_to_geo(geom: Geometry<'_>) -> Option<MultiPolygon<f64>> {
152-
match match geom.try_into() {
153-
Ok(c) => c,
154-
Err(e) => {
155-
warn!("convert_to_geo: conversion to geo failed: {}", e);
156-
return None;
155+
fn convert_to_geo(geom: Geometry<'_>) -> Result<Option<MultiPolygon<f64>>> {
156+
let is_empty_poly = |poly: &geo::Polygon| poly.exterior().lines().next().is_none();
157+
158+
let polys = match geom.try_into().context("failed to convert to geo")? {
159+
geo::Geometry::Polygon(x) => vec![x],
160+
geo::Geometry::GeometryCollection(geoms) => {
161+
// Convert each geometry into a multi-polygon
162+
let multi_polys: Vec<_> = geoms
163+
.into_iter()
164+
.map(MultiPolygon::try_from)
165+
.try_collect()?;
166+
167+
// Flatten all multi polygons into a single one
168+
multi_polys
169+
.into_iter()
170+
.flat_map(|m| m.into_iter())
171+
.collect()
157172
}
158-
} {
159-
geo::Geometry::Polygon(x) => Some(MultiPolygon(vec![x])),
160173
y => {
161-
if let Ok(x) = y.try_into() {
162-
Some(x)
163-
} else {
164-
None
165-
}
174+
return Ok(Some(
175+
y.try_into().context("failed to convert to multi-polygon")?,
176+
))
166177
}
167-
}
178+
};
179+
180+
let polys: Vec<_> = polys
181+
.into_iter()
182+
.filter(|poly| !is_empty_poly(poly))
183+
.collect();
184+
185+
Ok({
186+
if polys.is_empty() {
187+
None
188+
} else {
189+
Some(MultiPolygon(polys))
190+
}
191+
})
168192
}
169193

170-
fn subtract_existing_zones(zone: &mut Zone, to_subtract: &[&Zone]) -> Result<(), String> {
194+
fn subtract_existing_zones(zone: &mut Zone, to_subtract: &[&Zone]) -> Result<()> {
171195
if to_subtract.is_empty() {
172196
return Ok(());
173197
}
198+
174199
if let Some(ref boundary) = zone.boundary {
175200
let mut updates = 0;
176-
let mut g_boundary = match geos::Geometry::try_from(boundary) {
177-
Ok(b) => b,
178-
Err(e) => {
179-
warn!(
180-
"subtract_existing_town: failed to convert to geos for zone {}: {}",
181-
zone.osm_id, e
182-
);
183-
return Err(e.to_string());
184-
}
185-
};
201+
202+
let mut g_boundary = geos::Geometry::try_from(boundary).map_err(|err| {
203+
warn!(
204+
"subtract_existing_town: failed to convert to geos for zone {}: {}",
205+
zone.osm_id, err
206+
);
207+
err
208+
})?;
209+
186210
for z in to_subtract {
187211
if let Some(b) = difference(&g_boundary, z) {
188212
updates += 1;
189213
g_boundary = b;
190214
}
191215
}
216+
192217
if updates > 0 {
193-
match convert_to_geo(g_boundary) {
194-
Some(g) => {
195-
zone.bbox = g.bounding_rect();
196-
zone.boundary = Some(g);
197-
}
198-
None => {
199-
warn!(
200-
"subtract_existing_town: failed to convert back to geo for {}...",
201-
zone.osm_id
202-
);
203-
return Err("Failed to convert to Geo".to_owned());
204-
}
218+
if let Some(g) = convert_to_geo(g_boundary).map_err(|err| {
219+
warn!(
220+
"subtract_existing_town: failed to convert back to geo for {}...",
221+
zone.osm_id
222+
);
223+
err
224+
})? {
225+
zone.bbox = g.bounding_rect();
226+
zone.boundary = Some(g);
205227
}
206228
}
207229
}
208230
Ok(())
209231
}
210232

211-
fn get_zones_to_subtract<'a>(
233+
fn get_places_to_subtract<'a>(
212234
zone: &Zone,
213235
parent_id: &ZoneIndex,
214236
zones: &'a [Zone],
@@ -220,9 +242,7 @@ fn get_zones_to_subtract<'a>(
220242
.map(|z_idx| &zones[z_idx.index])
221243
.filter(|z| {
222244
z.admin_type()
223-
.map(|zt| {
224-
zt == ZoneType::City || (zt > ZoneType::City && z.parent == Some(*parent_id))
225-
})
245+
.map(|zt| Some(zt) == zone.zone_type || z.parent == Some(*parent_id))
226246
.unwrap_or(false)
227247
})
228248
.filter(|z| zone.intersects(z))
@@ -249,7 +269,7 @@ fn compute_voronoi(
249269
place.boundary = parent.boundary.clone();
250270
place.bbox = parent.bbox;
251271
place.parent = Some(parent.id);
252-
let zones_to_subtract = get_zones_to_subtract(parent, &parent.id, zones, zones_rtree);
272+
let zones_to_subtract = get_places_to_subtract(parent, &parent.id, zones, zones_rtree);
253273
// If an error occurs, we can't just use the parent area so instead, we return nothing.
254274
if subtract_existing_zones(&mut place, &zones_to_subtract).is_ok() {
255275
return vec![place];
@@ -361,13 +381,17 @@ fn compute_voronoi(
361381
match geos_parent.intersection(&voronoi) {
362382
Ok(s) => {
363383
place.parent = Some(parent.id);
364-
place.boundary = convert_to_geo(s);
384+
385+
place.boundary = convert_to_geo(s)
386+
.map_err(|err| warn!("failed to convert to geos: {err:?}"))
387+
.ok()
388+
.flatten();
365389

366390
if let Some(ref boundary) = place.boundary {
367391
place.bbox = boundary.bounding_rect();
368392
}
369393
let zones_to_subtract =
370-
get_zones_to_subtract(&place, &parent.id, zones, zones_rtree);
394+
get_places_to_subtract(&place, &parent.id, zones, zones_rtree);
371395
subtract_existing_zones(&mut place, &zones_to_subtract).ok()?;
372396
Some(place)
373397
}
@@ -387,7 +411,7 @@ fn compute_voronoi(
387411
.collect()
388412
}
389413

390-
fn publish_new_cities(zones: &mut Vec<Zone>, new_cities: Vec<Zone>) {
414+
fn publish_new_places(zones: &mut Vec<Zone>, new_cities: Vec<Zone>) {
391415
for mut city in new_cities {
392416
city.id = ZoneIndex { index: zones.len() };
393417
zones.push(city);

0 commit comments

Comments
 (0)