// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.pt_assistant.utils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.osm.BBox;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.plugins.pt_assistant.data.PTStop;
import org.openstreetmap.josm.plugins.pt_assistant.data.PTWay;
import org.openstreetmap.josm.tools.Pair;
/**
* Assigns stops to ways in following steps: (1) checks if the stop is in the
* list of already assigned stops, (2) checks if the stop has a stop position,
* (3) calculates it using proximity / growing bounding boxes
*
* @author darya
*
*/
public class StopToWayAssigner {
/* contains assigned stops */
public static HashMap<PTStop, List<Way>> stopToWay = new HashMap<>();
/*
* contains all PTWays of the route relation for which this assigner was
* created
*/
private HashSet<Way> ways;
/* route relation for which this StopToWayAssigner was created */
public StopToWayAssigner(List<PTWay> ptways) {
ways = new HashSet<>();
for (PTWay ptway : ptways) {
ways.addAll(ptway.getWays());
}
}
/**
* Returns the PTWay for the given PTStop
*
* @param stop stop
* @return the PTWay for the given PTStop
*/
public Way get(PTStop stop) {
// 1) Search if this stop has already been assigned:
if (stopToWay.containsKey(stop)) {
List<Way> assignedWays = stopToWay.get(stop);
for (Way assignedWay : assignedWays) {
if (this.ways.contains(assignedWay)) {
return assignedWay;
}
}
}
// 2) Search if the stop has a stop position:
Way wayOfStopPosition = findWayForNode(stop.getStopPosition());
if (wayOfStopPosition != null) {
addAssignedWayToMap(stop, wayOfStopPosition);
return wayOfStopPosition;
}
// 3) Search if the stop has a stop_area:
List<OsmPrimitive> stopElements = new ArrayList<>(2);
if (stop.getStopPosition() != null) {
stopElements.add(stop.getStopPosition());
}
if (stop.getPlatform() != null) {
stopElements.add(stop.getPlatform());
}
Set<Relation> parents = Node.getParentRelations(stopElements);
for (Relation parentRelation : parents) {
if (parentRelation.hasTag("public_transport", "stop_area")) {
for (RelationMember rm : parentRelation.getMembers()) {
if (rm.getMember().hasTag("public_transport", "stop_position")) {
Way rmWay = this.findWayForNode(rm.getNode());
if (rmWay != null) {
addAssignedWayToMap(stop, rmWay);
return rmWay;
}
}
}
}
}
// 4) Search if a stop position is in the vicinity of a platform:
if (stop.getPlatform() != null) {
List<Node> potentialStopPositionList = stop.findPotentialStopPositions();
Node closestStopPosition = null;
double minDistanceSq = Double.MAX_VALUE;
for (Node potentialStopPosition : potentialStopPositionList) {
double distanceSq = potentialStopPosition.getCoor()
.distanceSq(stop.getPlatform().getBBox().getCenter());
if (distanceSq < minDistanceSq) {
closestStopPosition = potentialStopPosition;
minDistanceSq = distanceSq;
}
}
if (closestStopPosition != null) {
Way closestWay = null;
double minDistanceSqToWay = Double.MAX_VALUE;
for (Way way: this.ways) {
if (way.containsNode(closestStopPosition)) {
double distanceSq = calculateMinDistanceToSegment(new Node(stop.getPlatform().getBBox().getCenter()), way);
if (distanceSq < minDistanceSqToWay) {
closestWay = way;
minDistanceSqToWay = distanceSq;
}
}
}
if (closestWay != null) {
addAssignedWayToMap(stop, closestWay);
return closestWay;
}
}
}
// 5) Run the growing-bounding-boxes algorithm:
double searchRadius = 0.001;
while (searchRadius < 0.005) {
Way foundWay = this.findNearestWayInRadius(stop.getPlatform(), searchRadius);
if (foundWay != null) {
addAssignedWayToMap(stop, foundWay);
return foundWay;
}
foundWay = this.findNearestWayInRadius(stop.getStopPosition(), searchRadius);
if (foundWay != null) {
addAssignedWayToMap(stop, foundWay);
return foundWay;
}
searchRadius = searchRadius + 0.001;
}
return null;
}
/**
* Finds the PTWay of the given stop_position by looking at its referrers
*
* @param stopPosition stop position
* @return the PTWay of the given stop_position by looking at its referrers
*/
private Way findWayForNode(Node stopPosition) {
if (stopPosition == null) {
return null;
}
// search in the referrers:
List<OsmPrimitive> referrers = stopPosition.getReferrers();
for (OsmPrimitive referredPrimitive : referrers) {
if (referredPrimitive.getType().equals(OsmPrimitiveType.WAY)) {
Way referredWay = (Way) referredPrimitive;
if (this.ways.contains(referredWay)) {
return referredWay;
}
}
}
return null;
}
/**
* Finds the PTWay in the given radius of the OsmPrimitive. The PTWay has to
* belong to the route relation for which this StopToWayAssigner was
* created. If multiple PTWays were found, the closest one is chosen.
*
* @param platform platform
* @param searchRadius search radius
* @return the PTWay in the given radius of the OsmPrimitive
*/
private Way findNearestWayInRadius(OsmPrimitive platform, double searchRadius) {
if (platform == null) {
return null;
}
LatLon platformCenter = platform.getBBox().getCenter();
Double ax = platformCenter.getX() - searchRadius;
Double bx = platformCenter.getX() + searchRadius;
Double ay = platformCenter.getY() - searchRadius;
Double by = platformCenter.getY() + searchRadius;
BBox platformBBox = new BBox(ax, ay, bx, by);
Set<Way> potentialWays = new HashSet<>();
Collection<Node> allNodes = platform.getDataSet().getNodes();
for (Node currentNode : allNodes) {
if (platformBBox.bounds(currentNode.getBBox())) {
List<OsmPrimitive> referrers = currentNode.getReferrers();
for (OsmPrimitive referrer : referrers) {
if (referrer.getType().equals(OsmPrimitiveType.WAY)) {
Way referrerWay = (Way) referrer;
if (this.ways.contains(referrerWay)) {
potentialWays.add(referrerWay);
}
}
}
}
}
Node platformNode = null;
if (platform.getType().equals(OsmPrimitiveType.NODE)) {
platformNode = (Node) platform;
} else {
platformNode = new Node(platform.getBBox().getCenter());
}
Way nearestWay = null;
Double minDistance = Double.MAX_VALUE;
for (Way potentialWay : potentialWays) {
double distance = this.calculateMinDistanceToSegment(platformNode, potentialWay);
if (distance < minDistance) {
minDistance = distance;
nearestWay = potentialWay;
}
}
return nearestWay;
}
/**
* Calculates the minimum distance between a node and a way
*
* @param node node
* @param way way
* @return the minimum distance between a node and a way
*/
private double calculateMinDistanceToSegment(Node node, Way way) {
double minDistance = Double.MAX_VALUE;
List<Pair<Node, Node>> waySegments = way.getNodePairs(false);
for (Pair<Node, Node> waySegment : waySegments) {
if (waySegment.a != node && waySegment.b != node) {
double distanceToLine = this.calculateDistanceToSegment(node, waySegment);
if (distanceToLine < minDistance) {
minDistance = distanceToLine;
}
}
}
return minDistance;
}
/**
* Calculates the distance from point to segment and differentiates between
* acute, right and obtuse triangles. If a triangle is acute or right, the
* distance to segment is calculated as distance from point to line. If the
* triangle is obtuse, the distance is calculated as the distance to the
* nearest vertex of the segment.
*
* @param node node
* @param segment segment
* @return the distance from point to segment
*/
private double calculateDistanceToSegment(Node node, Pair<Node, Node> segment) {
if (node == segment.a || node == segment.b) {
return 0.0;
}
double lengthA = node.getCoor().distance(segment.a.getCoor());
double lengthB = node.getCoor().distance(segment.b.getCoor());
double lengthC = segment.a.getCoor().distance(segment.b.getCoor());
if (isObtuse(lengthC, lengthB, lengthA)) {
return lengthB;
}
if (isObtuse(lengthA, lengthC, lengthB)) {
return lengthA;
}
return calculateDistanceToLine(node, segment);
}
/**
* Calculates the distance from point to line using formulas for triangle
* area. Does not differentiate between acute, right and obtuse triangles
*
* @param node node
* @param waySegment way segment
* @return the distance from point to line
*/
private double calculateDistanceToLine(Node node, Pair<Node, Node> segment) {
/*
* Let a be the triangle edge between the point and the first node of
* the segment. Let b be the triangle edge between the point and the
* second node of the segment. Let c be the triangle edge which is the
* segment.
*/
double lengthA = node.getCoor().distance(segment.a.getCoor());
double lengthB = node.getCoor().distance(segment.b.getCoor());
double lengthC = segment.a.getCoor().distance(segment.b.getCoor());
// calculate triangle area using Heron's formula:
double p = (lengthA + lengthB + lengthC) / 2.0;
double triangleArea = Math.sqrt(p * (p - lengthA) * (p - lengthB) * (p - lengthC));
// calculate the distance from point to segment using the 0.5*c*h
// formula for triangle area:
return triangleArea * 2.0 / lengthC;
}
/**
* Checks if the angle opposite of the edge c is obtuse. Uses the cosine
* theorem
*
* @param lengthA length A
* @param lengthB length B
* @param lengthC length C
* @return true if the angle opposite of the edge c is obtuse
*/
private boolean isObtuse(double lengthA, double lengthB, double lengthC) {
/*-
* Law of cosines:
* c^2 = a^2 + b^2 - 2abcos
* if c^2 = a^2 + b^2, it is a right triangle
* if c^2 < a^2 + b^2, it is an acute triangle
* if c^2 > a^2 + b^2, it is an obtuse triangle
*/
if (lengthC * lengthC > lengthA * lengthA + lengthB * lengthB) {
return true;
}
return false;
}
/**
* Adds the given way to the map of assigned ways. Assumes that the given
* way is not contained in the map.
*
* @param stop stop
* @param way way
*/
private void addAssignedWayToMap(PTStop stop, Way way) {
if (stopToWay.containsKey(stop)) {
List<Way> assignedWays = stopToWay.get(stop);
assignedWays.add(way);
} else {
List<Way> assignedWays = new ArrayList<>();
assignedWays.add(way);
stopToWay.put(stop, assignedWays);
}
}
/**
* May be needed if the correspondence between stops and ways has changed
* significantly
*/
public static void reinitiate() {
stopToWay = new HashMap<>();
}
}