Organize raw_map code a bit more. Let's cleanly separate the detection of short roads from transforming them. #654

And add a convenient script to diff changed maps. (Motivated by
preserving the fixpoint behavior in Montlake)
This commit is contained in:
Dustin Carlino 2022-02-20 12:01:46 +00:00
parent 58991ab041
commit 4583dac399
8 changed files with 218 additions and 201 deletions

View File

@ -101,7 +101,7 @@ impl MainState {
.build_def(ctx),
ctx.style()
.btn_outline
.text("auto mark junctions")
.text("detect short roads")
.build_def(ctx),
ctx.style()
.btn_outline
@ -256,8 +256,8 @@ impl State<App> for MainState {
"adjust boundary" => {
self.mode = Mode::SetBoundaryPt1;
}
"auto mark junctions" => {
for r in app.model.map.auto_mark_junctions() {
"detect short roads" => {
for r in app.model.map.find_traffic_signal_clusters() {
app.model.road_deleted(r);
app.model.road_added(ctx, r);
}

16
data/diff_changed_map.sh Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash
# If `updater --dry` says a mapchanged, call this to launch a UI and compare.
# This only works if you have a copy of the S3 directory in ~/s3_abst_data.
set -e
FILE=$1
if [ "$FILE" == "" ]; then
echo Missing args;
exit 1;
fi
rm -f old.bin
cp ~/s3_abst_data/dev/${FILE}.gz old.bin.gz
gunzip old.bin.gz
./target/release/game --dev $FILE --diff old.bin

View File

@ -316,47 +316,6 @@ impl RawMap {
.get(&to)
.cloned()
}
/// Look for short roads that should be merged, and mark them as junction=intersection.
pub fn auto_mark_junctions(&mut self) -> Vec<OriginalRoad> {
let threshold = Distance::meters(20.0);
// Simplest start: look for short roads connected to traffic signals.
//
// (This will miss sequences of short roads with stop signs in between a cluster of traffic
// signals)
//
// After trying out around Loop 101, what we really want to do is find clumps of 2 or 4
// traffic signals, find all the segments between them, and merge those.
let mut results = Vec::new();
for (id, road) in &self.roads {
if road.osm_tags.is("junction", "intersection") {
continue;
}
let i1 = self.intersections[&id.i1].intersection_type;
let i2 = self.intersections[&id.i2].intersection_type;
if i1 == IntersectionType::Border || i2 == IntersectionType::Border {
continue;
}
if i1 != IntersectionType::TrafficSignal && i2 != IntersectionType::TrafficSignal {
continue;
}
if let Ok((pl, _)) = road.get_geometry(*id, &self.config) {
if pl.length() <= threshold {
results.push(*id);
}
}
}
for id in &results {
self.roads
.get_mut(id)
.unwrap()
.osm_tags
.insert("junction", "intersection");
}
results
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]

View File

@ -0,0 +1,61 @@
use std::collections::BTreeSet;
use geom::Angle;
use crate::{osm, OriginalRoad, RawMap, RawRoad};
/// Does this road go between two divided one-ways? Ideally they're tagged explicitly
/// (https://wiki.openstreetmap.org/wiki/Tag:dual_carriageway%3Dyes), but we can also apply simple
/// heuristics to guess this.
#[allow(unused)]
pub fn connects_dual_carriageway(map: &RawMap, id: &OriginalRoad) -> bool {
let connectors_angle = angle(&map.roads[id]);
// There are false positives like https://www.openstreetmap.org/way/4636259 when we're looking
// at a segment along a marked dual carriageway. Filter out by requiring the intersecting dual
// carriageways to differ by a minimum angle.
let within_degrees = 10.0;
let mut i1_dual_carriageway = false;
let mut oneway_names_i1: BTreeSet<String> = BTreeSet::new();
for r in map.roads_per_intersection(id.i1) {
let road = &map.roads[&r];
if r == *id || connectors_angle.approx_eq(angle(road), within_degrees) {
continue;
}
if road.osm_tags.is("dual_carriageway", "yes") {
i1_dual_carriageway = true;
}
if road.osm_tags.is("oneway", "yes") {
if let Some(name) = road.osm_tags.get(osm::NAME) {
oneway_names_i1.insert(name.to_string());
}
}
}
let mut i2_dual_carriageway = false;
let mut oneway_names_i2: BTreeSet<String> = BTreeSet::new();
for r in map.roads_per_intersection(id.i2) {
let road = &map.roads[&r];
if r == *id || connectors_angle.approx_eq(angle(road), within_degrees) {
continue;
}
if road.osm_tags.is("dual_carriageway", "yes") {
i2_dual_carriageway = true;
}
if road.osm_tags.is("oneway", "yes") {
if let Some(name) = road.osm_tags.get(osm::NAME) {
oneway_names_i2.insert(name.to_string());
}
}
}
(i1_dual_carriageway && i2_dual_carriageway)
|| oneway_names_i1
.intersection(&oneway_names_i2)
.next()
.is_some()
}
fn angle(r: &RawRoad) -> Angle {
r.center_points[0].angle_to(*r.center_points.last().unwrap())
}

View File

@ -0,0 +1,99 @@
use abstutil::Timer;
use geom::Distance;
use crate::{IntersectionType, OriginalRoad, RawMap};
/// Combines a few different sources/methods to decide which roads are short. Marks them for
/// merging.
///
/// 1) Anything tagged in OSM
/// 2) Anything a temporary local merge_osm_ways.json file
/// 3) If `consolidate_all` is true, an experimental distance-based heuristic
pub fn find_short_roads(map: &mut RawMap, consolidate_all: bool) -> Vec<OriginalRoad> {
let mut roads = Vec::new();
for (id, road) in &map.roads {
if road.osm_tags.is("junction", "intersection") {
roads.push(*id);
continue;
}
if consolidate_all && distance_heuristic(*id, map) {
roads.push(*id);
}
}
// Use this to quickly test overrides to some ways before upstreaming in OSM.
// Since these IDs might be based on already merged roads, do these last.
if let Ok(ways) = abstio::maybe_read_json::<Vec<OriginalRoad>>(
"merge_osm_ways.json".to_string(),
&mut Timer::throwaway(),
) {
roads.extend(ways);
}
for id in &roads {
map.roads
.get_mut(id)
.unwrap()
.osm_tags
.insert("junction", "intersection");
}
roads
}
fn distance_heuristic(id: OriginalRoad, map: &RawMap) -> bool {
let road_length = if let Some(pl) = map.trimmed_road_geometry(id) {
pl.length()
} else {
// The road or something near it collapsed down into a single point or something. This can
// happen while merging several short roads around a single junction.
return false;
};
// Any road anywhere shorter than this should get merged.
road_length < Distance::meters(5.0)
}
impl RawMap {
/// A heuristic to find short roads near traffic signals
pub fn find_traffic_signal_clusters(&mut self) -> Vec<OriginalRoad> {
let threshold = Distance::meters(20.0);
// Simplest start: look for short roads connected to traffic signals.
//
// (This will miss sequences of short roads with stop signs in between a cluster of traffic
// signals)
//
// After trying out around Loop 101, what we really want to do is find clumps of 2 or 4
// traffic signals, find all the segments between them, and merge those.
let mut results = Vec::new();
for (id, road) in &self.roads {
if road.osm_tags.is("junction", "intersection") {
continue;
}
let i1 = self.intersections[&id.i1].intersection_type;
let i2 = self.intersections[&id.i2].intersection_type;
if i1 == IntersectionType::Border || i2 == IntersectionType::Border {
continue;
}
if i1 != IntersectionType::TrafficSignal && i2 != IntersectionType::TrafficSignal {
continue;
}
if let Ok((pl, _)) = road.get_geometry(*id, &self.config) {
if pl.length() <= threshold {
results.push(*id);
}
}
}
for id in &results {
self.roads
.get_mut(id)
.unwrap()
.osm_tags
.insert("junction", "intersection");
}
results
}
}

View File

@ -1,152 +0,0 @@
use std::collections::{BTreeSet, VecDeque};
use abstutil::Timer;
use geom::{Angle, Distance};
use crate::osm::NodeID;
use crate::{osm, OriginalRoad, RawMap, RawRoad};
/// Merge tiny "roads" that're actually just part of a complicated intersection. Returns all
/// surviving intersections adjacent to one of these merged roads.
pub fn merge_short_roads(map: &mut RawMap, consolidate_all: bool) -> BTreeSet<NodeID> {
#![allow(clippy::logic_bug)] // remove once the TODO below is taken care of
let mut merged = BTreeSet::new();
let mut queue: VecDeque<OriginalRoad> = VecDeque::new();
for r in map.roads.keys() {
queue.push_back(*r);
// TODO Enable after improving this heuristic.
if false && connects_dual_carriageway(map, r) {
debug!("{} connects dual carriageways", r);
}
}
while !queue.is_empty() {
let id = queue.pop_front().unwrap();
// The road might've been deleted
if !map.roads.contains_key(&id) {
continue;
}
if should_merge(map, &id, consolidate_all) {
match map.merge_short_road(id) {
Ok((i, _, _, new_roads)) => {
merged.insert(i);
queue.extend(new_roads);
}
Err(err) => {
warn!("Not merging short road / junction=intersection: {}", err);
}
}
}
}
// Use this to quickly test overrides to some ways before upstreaming in OSM.
// Since these IDs might be based on already merged roads, do these last.
if let Ok(ways) = abstio::maybe_read_json::<Vec<OriginalRoad>>(
"merge_osm_ways.json".to_string(),
&mut Timer::throwaway(),
) {
for id in ways {
match map.merge_short_road(id) {
Ok((i, _, _, _)) => {
merged.insert(i);
}
Err(err) => {
warn!("Not merging short road / junction=intersection: {}", err);
}
}
}
}
merged
}
fn should_merge(map: &RawMap, id: &OriginalRoad, consolidate_all: bool) -> bool {
// See https://wiki.openstreetmap.org/wiki/Proposed_features/junction%3Dintersection
if map.roads[id].osm_tags.is("junction", "intersection") {
return true;
}
// TODO Keep everything below disabled until merging works better.
if !consolidate_all {
return false;
}
let road_length = if let Some(pl) = map.trimmed_road_geometry(*id) {
pl.length()
} else {
// The road or something near it collapsed down into a single point or something. This can
// happen while merging several short roads around a single junction.
return false;
};
// Any road anywhere shorter than this should get merged.
if road_length < Distance::meters(5.0) {
return true;
}
// Roads connecting dual carriageways can use a longer threshold for merging.
if connects_dual_carriageway(map, id) && road_length < Distance::meters(10.0) {
return true;
}
false
}
// Does this road go between two divided one-ways? Ideally they're tagged explicitly
// (https://wiki.openstreetmap.org/wiki/Tag:dual_carriageway%3Dyes), but we can also apply simple
// heuristics to guess this.
fn connects_dual_carriageway(map: &RawMap, id: &OriginalRoad) -> bool {
let connectors_angle = angle(&map.roads[id]);
// There are false positives like https://www.openstreetmap.org/way/4636259 when we're looking
// at a segment along a marked dual carriageway. Filter out by requiring the intersecting dual
// carriageways to differ by a minimum angle.
let within_degrees = 10.0;
let mut i1_dual_carriageway = false;
let mut oneway_names_i1: BTreeSet<String> = BTreeSet::new();
for r in map.roads_per_intersection(id.i1) {
let road = &map.roads[&r];
if r == *id || connectors_angle.approx_eq(angle(road), within_degrees) {
continue;
}
if road.osm_tags.is("dual_carriageway", "yes") {
i1_dual_carriageway = true;
}
if road.osm_tags.is("oneway", "yes") {
if let Some(name) = road.osm_tags.get(osm::NAME) {
oneway_names_i1.insert(name.to_string());
}
}
}
let mut i2_dual_carriageway = false;
let mut oneway_names_i2: BTreeSet<String> = BTreeSet::new();
for r in map.roads_per_intersection(id.i2) {
let road = &map.roads[&r];
if r == *id || connectors_angle.approx_eq(angle(road), within_degrees) {
continue;
}
if road.osm_tags.is("dual_carriageway", "yes") {
i2_dual_carriageway = true;
}
if road.osm_tags.is("oneway", "yes") {
if let Some(name) = road.osm_tags.get(osm::NAME) {
oneway_names_i2.insert(name.to_string());
}
}
}
(i1_dual_carriageway && i2_dual_carriageway)
|| oneway_names_i1
.intersection(&oneway_names_i2)
.next()
.is_some()
}
fn angle(r: &RawRoad) -> Angle {
r.center_points[0].angle_to(*r.center_points.last().unwrap())
}

View File

@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::collections::{BTreeMap, VecDeque};
use anyhow::Result;
@ -71,8 +71,6 @@ impl RawMap {
continue;
}
// If we're going to delete this later, don't bother!
// TODO When we do automatic consolidation and don't just look for this tag,
// we'll need to get more clever here, or temporarily apply this tag.
if self.roads[&r].osm_tags.is("junction", "intersection") {
continue;
}
@ -187,3 +185,37 @@ impl RawMap {
Ok((i1, i2, deleted, created))
}
}
/// Merge all roads marked with `junction=intersection`
pub fn merge_all_junctions(map: &mut RawMap) {
let mut queue: VecDeque<OriginalRoad> = VecDeque::new();
for (id, road) in &map.roads {
if road.osm_tags.is("junction", "intersection") {
queue.push_back(*id);
}
}
while !queue.is_empty() {
let id = queue.pop_front().unwrap();
// The road might've been deleted by a previous merge_short_road call
if !map.roads.contains_key(&id) {
continue;
}
match map.merge_short_road(id) {
Ok((_, _, _, new_roads)) => {
// Some road IDs still in the queue might have changed, so check the new_roads for
// anything we should try to merge
for r in new_roads {
if map.roads[&r].osm_tags.is("junction", "intersection") {
queue.push_back(r);
}
}
}
Err(err) => {
warn!("Not merging short road / junction=intersection: {}", err);
}
}
}
}

View File

@ -3,7 +3,8 @@ use abstutil::Timer;
use crate::RawMap;
mod collapse_intersections;
mod merge_intersections;
mod dual_carriageways;
mod find_short_roads;
mod merge_short_road;
mod remove_disconnected;
mod snappy;
@ -35,7 +36,8 @@ impl RawMap {
remove_disconnected::remove_disconnected_roads(self, timer);
timer.start("merging short roads");
merge_intersections::merge_short_roads(self, consolidate_all_intersections);
find_short_roads::find_short_roads(self, consolidate_all_intersections);
merge_short_road::merge_all_junctions(self);
timer.stop("merging short roads");
timer.start("collapsing degenerate intersections");