-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Crosswalk naming #6918
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Crosswalk naming #6918
Changes from 27 commits
e11409d
28b188f
38f5e11
6b83df3
21e3289
a4689cb
989f7c2
1daa299
aeb0547
38f0060
347e72d
cb6e68e
1acf57c
b52cedb
602cb89
f5003d0
9560214
fb5bc8e
109f15d
c760b69
e03a998
146fbb8
3cb0082
2027866
dc99a18
3492317
1f2d8f2
7cd0ec3
c75f928
e6351db
31a9e64
340b5fc
3c92825
cf97687
615e303
5fd15af
320ee6d
df13ef6
93f81ab
555c188
685adfd
fce3f0c
ba3c149
241a85a
f5b2081
954cf59
bd8de75
3072289
334a0cd
7d52252
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| package org.opentripplanner.graph_builder.module.osm.naming; | ||
|
|
||
| import static org.opentripplanner.osm.model.TraverseDirection.FORWARD; | ||
|
|
||
| import gnu.trove.list.TLongList; | ||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.Collection; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
| import org.locationtech.jts.geom.Geometry; | ||
| import org.opentripplanner.framework.geometry.HashGridSpatialIndex; | ||
| import org.opentripplanner.framework.i18n.I18NString; | ||
| import org.opentripplanner.graph_builder.module.osm.StreetEdgePair; | ||
| import org.opentripplanner.osm.model.OsmEntity; | ||
| import org.opentripplanner.osm.model.OsmWay; | ||
| import org.opentripplanner.osm.model.TraverseDirection; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** | ||
| * A namer that assigns names to crosswalks using the name or type of the crossed street. | ||
| * <p> | ||
| * The algorithm works as follows: | ||
| * - For each crosswalk, we find the intersecting street edge that shares a node. | ||
| * - Apply a name depending on the type of street: | ||
| * * For named streets, name the crossing so it reads "crossing over 10th Street". | ||
| * * For service roads (e.g. car access to commercial complexes, such as | ||
| * <a href="https://www.openstreetmap.org/way/1024601318">...</a>), | ||
| * use "crossing over service road". | ||
| * * For turn lanes or slip lanes at intersections (shortcuts from a street to another, | ||
| * to bypass traffic signals, prevalent in North America, | ||
| * e.g. <a href="https://www.openstreetmap.org/way/1139062913">...</a>), | ||
| * use "crossing over turn lane". | ||
| */ | ||
| public class CrosswalkNamer extends NamerWithGeoBuffer { | ||
|
|
||
| private static final Logger LOG = LoggerFactory.getLogger(CrosswalkNamer.class); | ||
| private static final int BUFFER_METERS = 25; | ||
|
|
||
| private HashGridSpatialIndex<EdgeOnLevel> streetEdges = new HashGridSpatialIndex<>(); | ||
| private HashGridSpatialIndex<EdgeOnLevel> sidewalkEdges = new HashGridSpatialIndex<>(); | ||
| private Collection<EdgeOnLevel> unnamedCrosswalks = new ArrayList<>(); | ||
|
|
||
| @Override | ||
| public void recordEdges(OsmEntity way, StreetEdgePair pair) { | ||
| if (way instanceof OsmWay osmWay) { | ||
| // Record unnamed crossings to a list. | ||
| if (osmWay.isCrossing() && way.hasNoName() && !way.isExplicitlyUnnamed()) { | ||
| pair | ||
| .asIterable() | ||
| .forEach(edge -> unnamedCrosswalks.add(new EdgeOnLevel(osmWay, edge, way.getLevels()))); | ||
| } | ||
| // Record (short) sidewalks to a geometric index | ||
| else if (way.isSidewalk()) { | ||
| addToSpatialIndex(way, pair, sidewalkEdges, BUFFER_METERS); | ||
| } | ||
| // Record named streets, service roads, and slip/turn lanes to a geometric index. | ||
| else if ( | ||
| !osmWay.isFootway() && (way.isNamed() || osmWay.isServiceRoad() || isTurnLane(osmWay)) | ||
| ) { | ||
| addToSpatialIndex(way, pair, streetEdges); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void postprocess() { | ||
| postprocess(unnamedCrosswalks, BUFFER_METERS, "crosswalks", LOG); | ||
|
|
||
| // Set the indices to null so they can be garbage-collected | ||
| streetEdges = null; | ||
| sidewalkEdges = null; | ||
| unnamedCrosswalks = null; | ||
| } | ||
|
|
||
| /** | ||
| * The actual worker method that runs the business logic on an individual sidewalk edge. | ||
| * This will also name adjacent sidewalks on each end if they are the only adjacent sidewalks. | ||
| */ | ||
| @Override | ||
| protected boolean assignNameToEdge(EdgeOnLevel crosswalkOnLevel, Geometry buffer) { | ||
| var crosswalk = crosswalkOnLevel.edge(); | ||
| OsmWay way = crosswalkOnLevel.way(); | ||
|
|
||
| var streetCandidates = streetEdges | ||
| .query(buffer.getEnvelopeInternal()) | ||
| .stream() | ||
| .map(EdgeOnLevel::way) | ||
| .toList(); | ||
|
|
||
| var crossStreetOpt = getIntersectingStreet(way, streetCandidates); | ||
| if (crossStreetOpt.isPresent()) { | ||
| OsmWay crossStreet = crossStreetOpt.get(); | ||
| // TODO: i18n | ||
| if (crossStreet.isNamed()) { | ||
| crosswalk.setName( | ||
| I18NString.of(String.format("crossing over %s", crossStreet.getAssumedName())) | ||
| ); | ||
| } else if (crossStreet.isServiceRoad()) { | ||
| crosswalk.setName(I18NString.of("crossing over service road")); | ||
| } else if (isTurnLane(crossStreet)) { | ||
| crosswalk.setName(I18NString.of("crossing over turn lane")); | ||
| } else { | ||
| // Default on using the OSM way ID, which should not happen. | ||
| crosswalk.setName(I18NString.of(String.format("crossing %s", way.getId()))); | ||
| } | ||
|
|
||
| var adjacentSidewalks = sidewalkEdges | ||
| .query(buffer.getEnvelopeInternal()) | ||
| .stream() | ||
| .filter(e -> e.way().isAdjacentTo(way)) | ||
| .filter(e -> e.edge().nameIsDerived()) | ||
| .toList(); | ||
|
|
||
| // Group sidewalks at each end of the crosswalk. | ||
| TLongList nodes = way.getNodeRefs(); | ||
| renameAdjacentSidewalk(adjacentSidewalks, crosswalk.getName(), nodes.get(0)); | ||
| renameAdjacentSidewalk(adjacentSidewalks, crosswalk.getName(), nodes.get(nodes.size() - 1)); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Rename a sidewalk, among candidates, if it is the only adjacent sidewalk to the given crosswalk. | ||
| */ | ||
| private void renameAdjacentSidewalk( | ||
| List<EdgeOnLevel> adjacentSidewalks, | ||
| I18NString crosswalkName, | ||
| long nodeId | ||
| ) { | ||
| List<EdgeOnLevel> sidewalks = adjacentSidewalks | ||
| .stream() | ||
| .filter(e -> e.way().getNodeRefs().contains(nodeId)) | ||
| .toList(); | ||
| if (sidewalks.size() == 1) { | ||
| sidewalks.getFirst().edge().setName(crosswalkName); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Gets the intersecting street, if any, for the given way and candidate streets. | ||
| */ | ||
| public static Optional<OsmWay> getIntersectingStreet(OsmWay way, Collection<OsmWay> streets) { | ||
| TLongList nodeRefs = way.getNodeRefs(); | ||
| if (nodeRefs.size() >= 3) { | ||
| // There needs to be at least three nodes: 2 extremities that are on the sidewalk, | ||
| // and one somewhere in the middle that joins the crossing with the street. | ||
| // We exclude the first and last node which are on the sidewalk. | ||
| long[] nodeRefsArray = nodeRefs.toArray(1, nodeRefs.size() - 2); | ||
| return streets | ||
| .stream() | ||
| .filter(w -> Arrays.stream(nodeRefsArray).anyMatch(nid -> w.getNodeRefs().contains(nid))) | ||
| .findFirst(); | ||
| } | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| private static boolean isTurnLane(OsmEntity way) { | ||
| Optional<TraverseDirection> oneWayCar = way.isOneWay("motorcar"); | ||
| return oneWayCar.isPresent() && oneWayCar.get() == FORWARD; | ||
| } | ||
|
||
|
|
||
| public Collection<EdgeOnLevel> getUnnamedCrosswalks() { | ||
| return unnamedCrosswalks; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| package org.opentripplanner.graph_builder.module.osm.naming; | ||
|
|
||
| import java.util.Collection; | ||
| import java.util.Set; | ||
| import java.util.concurrent.atomic.AtomicInteger; | ||
| import org.geotools.api.referencing.FactoryException; | ||
| import org.geotools.api.referencing.crs.CoordinateReferenceSystem; | ||
| import org.geotools.api.referencing.operation.MathTransform; | ||
| import org.geotools.api.referencing.operation.TransformException; | ||
| import org.geotools.geometry.jts.JTS; | ||
| import org.geotools.referencing.CRS; | ||
| import org.geotools.referencing.crs.DefaultGeographicCRS; | ||
| import org.locationtech.jts.geom.Coordinate; | ||
| import org.locationtech.jts.geom.Envelope; | ||
| import org.locationtech.jts.geom.Geometry; | ||
| import org.locationtech.jts.operation.buffer.BufferParameters; | ||
| import org.opentripplanner.framework.geometry.HashGridSpatialIndex; | ||
| import org.opentripplanner.framework.i18n.I18NString; | ||
| import org.opentripplanner.graph_builder.module.osm.StreetEdgePair; | ||
| import org.opentripplanner.graph_builder.services.osm.EdgeNamer; | ||
| import org.opentripplanner.osm.model.OsmEntity; | ||
| import org.opentripplanner.osm.model.OsmWay; | ||
| import org.opentripplanner.street.model.edge.StreetEdge; | ||
| import org.opentripplanner.utils.lang.DoubleUtils; | ||
| import org.opentripplanner.utils.logging.ProgressTracker; | ||
| import org.slf4j.Logger; | ||
|
|
||
| /** | ||
| * Base class for namers that use a geo buffer to query geo features. | ||
| */ | ||
| public abstract class NamerWithGeoBuffer implements EdgeNamer { | ||
t2gran marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| protected PreciseBuffer preciseBuffer; | ||
|
|
||
| @Override | ||
| public I18NString name(OsmEntity way) { | ||
| return way.getAssumedName(); | ||
| } | ||
|
|
||
| protected void postprocess( | ||
binh-dam-ibigroup marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Collection<EdgeOnLevel> unnamedEdges, | ||
| int bufferMeters, | ||
| String type, | ||
| Logger logger | ||
| ) { | ||
| ProgressTracker progress = ProgressTracker.track( | ||
| String.format("Assigning names to %s", type), | ||
| 500, | ||
| unnamedEdges.size() | ||
| ); | ||
|
|
||
| this.preciseBuffer = new PreciseBuffer(computeEnvelopeCenter(unnamedEdges), bufferMeters); | ||
|
|
||
| final AtomicInteger namesApplied = new AtomicInteger(0); | ||
| unnamedEdges | ||
| .parallelStream() | ||
| .forEach(edgeOnLevel -> { | ||
| var buffer = preciseBuffer.preciseBuffer(edgeOnLevel.edge.getGeometry()); | ||
| if (assignNameToEdge(edgeOnLevel, buffer)) { | ||
| namesApplied.incrementAndGet(); | ||
| } | ||
|
|
||
| // Keep lambda! A method-ref would cause incorrect class and line number to be logged | ||
| // noinspection Convert2MethodRef | ||
| progress.step(m -> logger.info(m)); | ||
| }); | ||
|
|
||
| logger.info( | ||
| "Assigned names to {} of {} {} ({}%)", | ||
| namesApplied.get(), | ||
| unnamedEdges.size(), | ||
| type, | ||
| DoubleUtils.roundTo2Decimals(((double) namesApplied.get() / unnamedEdges.size()) * 100) | ||
| ); | ||
|
|
||
| logger.info(progress.completeMessage()); | ||
| } | ||
|
|
||
| /** | ||
| * Implementation-specific logic for naming an edge. | ||
| * @return true if a name was applied, false otherwise. | ||
| */ | ||
| protected abstract boolean assignNameToEdge(EdgeOnLevel edgeOnLevel, Geometry buffer); | ||
|
|
||
| /** | ||
| * Compute the centroid of all sidewalk edges. | ||
| */ | ||
| private Coordinate computeEnvelopeCenter(Collection<EdgeOnLevel> edges) { | ||
| var envelope = new Envelope(); | ||
| edges.forEach(e -> { | ||
| envelope.expandToInclude(e.edge.getFromVertex().getCoordinate()); | ||
| envelope.expandToInclude(e.edge.getToVertex().getCoordinate()); | ||
| }); | ||
| return envelope.centre(); | ||
| } | ||
|
|
||
| /** | ||
| * Adds an entry to a geospatial index. | ||
| */ | ||
| protected static void addToSpatialIndex( | ||
| OsmEntity way, | ||
| StreetEdgePair pair, | ||
| HashGridSpatialIndex<EdgeOnLevel> spatialIndex | ||
| ) { | ||
| addToSpatialIndex(way, pair, spatialIndex, Integer.MAX_VALUE); | ||
| } | ||
|
|
||
| /** | ||
| * Adds an entry to a geospatial index if its length is less than a threshold. | ||
| */ | ||
| protected static void addToSpatialIndex( | ||
| OsmEntity way, | ||
| StreetEdgePair pair, | ||
| HashGridSpatialIndex<EdgeOnLevel> spatialIndex, | ||
| int maxLengthMeters | ||
| ) { | ||
| // We generate two edges for each osm way: one there and one back. This spatial index only | ||
| // needs to contain one item for each road segment with a unique geometry and name, so we | ||
| // add only one of the two edges. | ||
| var edge = pair.pickAny(); | ||
| if (edge.getDistanceMeters() <= maxLengthMeters) { | ||
| spatialIndex.insert( | ||
| edge.getGeometry().getEnvelopeInternal(), | ||
| new EdgeOnLevel((OsmWay) way, edge, way.getLevels()) | ||
| ); | ||
| } | ||
| } | ||
binh-dam-ibigroup marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| public record EdgeOnLevel(OsmWay way, StreetEdge edge, Set<String> levels) {} | ||
binh-dam-ibigroup marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * A class to cache the expensive construction of a Universal Traverse Mercator coordinate | ||
| * reference system. | ||
| * Re-using the same CRS for all edges might introduce tiny imprecisions for OTPs use cases | ||
| * but speeds up the processing enormously and is a price well worth paying. | ||
| */ | ||
| protected static final class PreciseBuffer { | ||
binh-dam-ibigroup marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| private final double distanceInMeters; | ||
| private final MathTransform toTransform; | ||
| private final MathTransform fromTransform; | ||
|
|
||
| private PreciseBuffer(Coordinate coordinate, double distanceInMeters) { | ||
| this.distanceInMeters = distanceInMeters; | ||
| String code = "AUTO:42001,%s,%s".formatted(coordinate.x, coordinate.y); | ||
| try { | ||
| CoordinateReferenceSystem auto = CRS.decode(code); | ||
| this.toTransform = CRS.findMathTransform(DefaultGeographicCRS.WGS84, auto); | ||
| this.fromTransform = CRS.findMathTransform(auto, DefaultGeographicCRS.WGS84); | ||
| } catch (FactoryException e) { | ||
| throw new RuntimeException(e); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Add a buffer around a geometry that makes sure that the buffer is the same distance (in | ||
| * meters) anywhere on earth. | ||
| * <p> | ||
| * Background: If you call the regular buffer() method on a JTS geometry that uses WGS84 as the | ||
| * coordinate reference system, the buffer will be accurate at the equator but will become more | ||
| * and more elongated the farther north/south you go. | ||
| * <p> | ||
| * Taken from https://stackoverflow.com/questions/36455020 | ||
| */ | ||
| protected Geometry preciseBuffer(Geometry geometry) { | ||
| try { | ||
| Geometry pGeom = JTS.transform(geometry, toTransform); | ||
| Geometry pBufferedGeom = pGeom.buffer(distanceInMeters, 4, BufferParameters.CAP_FLAT); | ||
| return JTS.transform(pBufferedGeom, fromTransform); | ||
| } catch (TransformException e) { | ||
| throw new RuntimeException(e); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package org.opentripplanner.graph_builder.module.osm.naming; | ||
|
|
||
| import org.opentripplanner.framework.i18n.I18NString; | ||
| import org.opentripplanner.graph_builder.module.osm.StreetEdgePair; | ||
| import org.opentripplanner.graph_builder.services.osm.EdgeNamer; | ||
| import org.opentripplanner.osm.model.OsmEntity; | ||
|
|
||
| /** | ||
| * Combines the sidewalk and crosswalk namer. | ||
| */ | ||
| public class SidewalkCrosswalkNamer implements EdgeNamer { | ||
|
|
||
| private final SidewalkNamer sidewalkNamer = new SidewalkNamer(); | ||
| private final CrosswalkNamer crosswalkNamer = new CrosswalkNamer(); | ||
|
|
||
| @Override | ||
| public I18NString name(OsmEntity way) { | ||
| return way.getAssumedName(); | ||
| } | ||
|
|
||
| @Override | ||
| public void recordEdges(OsmEntity way, StreetEdgePair pair) { | ||
| sidewalkNamer.recordEdges(way, pair); | ||
| crosswalkNamer.recordEdges(way, pair); | ||
| } | ||
|
|
||
| @Override | ||
| public void postprocess() { | ||
| sidewalkNamer.postprocess(); | ||
| crosswalkNamer.postprocess(); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.