
org.opentripplanner.graph_builder.module.osm.OsmArea Maven / Gradle / Ivy
The newest version!
package org.opentripplanner.graph_builder.module.osm;
import com.google.common.collect.ArrayListMultimap;
import gnu.trove.list.TLongList;
import gnu.trove.list.array.TLongArrayList;
import gnu.trove.map.TLongObjectMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.TopologyException;
import org.locationtech.jts.operation.valid.IsValidOp;
import org.locationtech.jts.operation.valid.TopologyValidationError;
import org.opentripplanner.framework.geometry.GeometryUtils;
import org.opentripplanner.osm.model.OsmEntity;
import org.opentripplanner.osm.model.OsmNode;
import org.opentripplanner.osm.model.OsmWay;
/**
* Stores information about an OSM area needed for visibility graph construction. Algorithm based on
* http://wiki.openstreetmap.org/wiki/Relation:multipolygon/Algorithm but generally done in a
* quick/dirty way.
*/
class OsmArea {
final List outermostRings;
// This is the way or relation that has the relevant tags for the area
final OsmEntity parent;
public MultiPolygon jtsMultiPolygon;
OsmArea(
OsmEntity parent,
List outerRingWays,
List innerRingWays,
TLongObjectMap nodes
) {
this.parent = parent;
// ring assignment
List innerRingNodes = constructRings(innerRingWays);
List outerRingNodes = constructRings(outerRingWays);
if (innerRingNodes == null || outerRingNodes == null) {
throw new AreaConstructionException("innerRing or outerRing nodes are null");
}
ArrayList allRings = new ArrayList<>(innerRingNodes);
allRings.addAll(outerRingNodes);
List innerRings = new ArrayList<>();
List outerRings = new ArrayList<>();
for (TLongList ring : innerRingNodes) {
innerRings.add(new Ring(ring, nodes));
}
for (TLongList ring : outerRingNodes) {
outerRings.add(new Ring(ring, nodes));
}
List outermostRings = new ArrayList<>();
try {
// now, ring grouping
// first, find outermost rings
OUTER: for (Ring outer : outerRings) {
for (Ring possibleContainer : outerRings) {
if (outer != possibleContainer && outer.jtsPolygon.within(possibleContainer.jtsPolygon)) {
continue OUTER;
}
}
outermostRings.add(outer);
// find holes in this ring
for (Ring possibleHole : innerRings) {
if (possibleHole.jtsPolygon.within(outer.jtsPolygon)) {
outer.addHole(possibleHole);
}
}
}
} catch (TopologyException ex) {
throw new AreaConstructionException(ex.getMessage());
}
// Make outermostRings immutable
this.outermostRings = List.copyOf(outermostRings);
// run this at end of ctor so that exception
// can be caught in the right place
jtsMultiPolygon = calculateJTSMultiPolygon();
}
public List constructRings(List ways) {
if (ways.size() == 0) {
// no rings is no rings
return Collections.emptyList();
}
List closedRings = new ArrayList<>();
ArrayListMultimap waysByEndpoint = ArrayListMultimap.create();
for (OsmWay way : ways) {
TLongList refs = way.getNodeRefs();
long start = refs.get(0);
long end = refs.get(refs.size() - 1);
if (start == end) {
TLongList ring = new TLongArrayList(refs);
closedRings.add(ring);
} else {
waysByEndpoint.put(start, way);
waysByEndpoint.put(end, way);
}
}
// Precheck for impossible situations, and remove those.
TLongList endpointsToRemove = new TLongArrayList();
for (Long endpoint : waysByEndpoint.keySet()) {
Collection list = waysByEndpoint.get(endpoint);
if (list.size() % 2 == 1) {
endpointsToRemove.add(endpoint);
}
}
endpointsToRemove.forEach(endpoint -> {
waysByEndpoint.removeAll(endpoint);
return true;
});
TLongList partialRing = new TLongArrayList();
if (waysByEndpoint.size() == 0) {
return closedRings;
}
long firstEndpoint = 0, otherEndpoint = 0;
OsmWay firstWay = null;
for (Long endpoint : waysByEndpoint.keySet()) {
List list = waysByEndpoint.get(endpoint);
firstWay = list.get(0);
TLongList nodeRefs = firstWay.getNodeRefs();
partialRing.addAll(nodeRefs);
firstEndpoint = nodeRefs.get(0);
otherEndpoint = nodeRefs.get(nodeRefs.size() - 1);
break;
}
waysByEndpoint.get(firstEndpoint).remove(firstWay);
waysByEndpoint.get(otherEndpoint).remove(firstWay);
if (constructRingsRecursive(waysByEndpoint, partialRing, closedRings, firstEndpoint)) {
return closedRings;
} else {
return null;
}
}
/**
* Try to extract a point which is in the middle of the area and
* also inside the area geometry.
*
* @return Point geometry inside the area
*/
public Point findInteriorPoint() {
var centroid = jtsMultiPolygon.getCentroid();
if (jtsMultiPolygon.intersects(centroid)) {
return centroid;
}
return jtsMultiPolygon.getInteriorPoint();
}
private MultiPolygon calculateJTSMultiPolygon() {
List polygons = new ArrayList<>();
for (Ring ring : outermostRings) {
polygons.add(ring.jtsPolygon);
}
MultiPolygon jtsMultiPolygon = GeometryUtils.getGeometryFactory()
.createMultiPolygon(polygons.toArray(new Polygon[0]));
var validOp = new IsValidOp(jtsMultiPolygon);
if (!validOp.isValid()) {
var validationError = validOp.getValidationError();
throw new AreaConstructionException(
"%s at %s".formatted(validationError.getMessage(), validationError.getCoordinate())
);
}
return jtsMultiPolygon;
}
private boolean constructRingsRecursive(
ArrayListMultimap waysByEndpoint,
TLongList ring,
List closedRings,
long endpoint
) {
List ways = new ArrayList<>(waysByEndpoint.get(endpoint));
for (OsmWay way : ways) {
// remove this way from the map
TLongList nodeRefs = way.getNodeRefs();
long firstEndpoint = nodeRefs.get(0);
long otherEndpoint = nodeRefs.get(nodeRefs.size() - 1);
waysByEndpoint.remove(firstEndpoint, way);
waysByEndpoint.remove(otherEndpoint, way);
TLongList newRing = new TLongArrayList(ring.size() + nodeRefs.size());
long newFirstEndpoint;
if (firstEndpoint == endpoint) {
for (int j = nodeRefs.size() - 1; j >= 1; --j) {
newRing.add(nodeRefs.get(j));
}
newRing.addAll(ring);
newFirstEndpoint = otherEndpoint;
} else {
newRing.addAll(nodeRefs.subList(0, nodeRefs.size() - 1));
newRing.addAll(ring);
newFirstEndpoint = firstEndpoint;
}
if (newRing.get(newRing.size() - 1) == (newRing.get(0))) {
// ring closure
closedRings.add(newRing);
// if we're out of endpoints, then we have succeeded
if (waysByEndpoint.size() == 0) {
return true; // success
}
// otherwise, we need to start a new partial ring
newRing = new TLongArrayList();
OsmWay firstWay = null;
for (Long entry : waysByEndpoint.keySet()) {
List list = waysByEndpoint.get(entry);
firstWay = list.get(0);
nodeRefs = firstWay.getNodeRefs();
newRing.addAll(nodeRefs);
firstEndpoint = nodeRefs.get(0);
otherEndpoint = nodeRefs.get(nodeRefs.size() - 1);
break;
}
waysByEndpoint.remove(firstEndpoint, firstWay);
waysByEndpoint.remove(otherEndpoint, firstWay);
if (constructRingsRecursive(waysByEndpoint, newRing, closedRings, firstEndpoint)) {
return true;
}
waysByEndpoint.remove(firstEndpoint, firstWay);
waysByEndpoint.remove(otherEndpoint, firstWay);
} else {
// continue with this ring
if (waysByEndpoint.get(newFirstEndpoint) != null) {
if (constructRingsRecursive(waysByEndpoint, newRing, closedRings, newFirstEndpoint)) {
return true;
}
}
}
if (firstEndpoint == endpoint) {
waysByEndpoint.put(otherEndpoint, way);
} else {
waysByEndpoint.put(firstEndpoint, way);
}
}
return false;
}
public static class AreaConstructionException extends RuntimeException {
private final String message;
public AreaConstructionException(String message) {
this.message = message;
}
@Override
public String getMessage() {
return message;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy