// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.pt_assistant.validation;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.openstreetmap.josm.command.ChangeCommand;
import org.openstreetmap.josm.command.Command;
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.data.validation.Severity;
import org.openstreetmap.josm.data.validation.Test;
import org.openstreetmap.josm.data.validation.TestError;
import org.openstreetmap.josm.data.validation.TestError.Builder;
import org.openstreetmap.josm.plugins.pt_assistant.utils.RouteUtils;
/**
* Performs tests of a route at the level of single ways: DirectionTest and
* RoadTypeTest
*
* @author darya
*
*/
public class WayChecker extends Checker {
public WayChecker(Relation relation, Test test) {
super(relation, test);
}
protected void performRoadTypeTest() {
if (!relation.hasTag("route", "bus") && !relation.hasTag("route", "trolleybus")
&& !relation.hasTag("route", "share_taxi")) {
return;
}
for (RelationMember rm : relation.getMembers()) {
if (RouteUtils.isPTWay(rm) && rm.getType().equals(OsmPrimitiveType.WAY)) {
Way way = rm.getWay();
// at this point, the relation has already been checked to
// be a route of public_transport:version 2
boolean isCorrectRoadType = true;
boolean isUnderConstruction = false;
if (way.hasKey("construction")) {
isUnderConstruction = true;
}
if (relation.hasTag("route", "bus") || relation.hasTag("route", "share_taxi")) {
if (!RouteUtils.isWaySuitableForBuses(way)) {
isCorrectRoadType = false;
}
if (way.hasTag("highway", "construction")) {
isUnderConstruction = true;
}
} else if (relation.hasTag("route", "trolleybus")) {
if (!(RouteUtils.isWaySuitableForBuses(way) && way.hasTag("trolley_wire", "yes"))) {
isCorrectRoadType = false;
}
if (way.hasTag("highway", "construction")) {
isUnderConstruction = true;
}
} else if (relation.hasTag("route", "tram")) {
if (!way.hasTag("railway", "tram")) {
isCorrectRoadType = false;
}
if (way.hasTag("railway", "construction")) {
isUnderConstruction = true;
}
} else if (relation.hasTag("route", "subway")) {
if (!way.hasTag("railway", "subway")) {
isCorrectRoadType = false;
}
if (way.hasTag("railway", "construction")) {
isUnderConstruction = true;
}
} else if (relation.hasTag("route", "light_rail")) {
if (!way.hasTag("railway", "subway")) {
isCorrectRoadType = false;
}
if (way.hasTag("railway", "construction")) {
isUnderConstruction = true;
}
} else if (relation.hasTag("route", "light_rail")) {
if (!way.hasTag("railway", "light_rail")) {
isCorrectRoadType = false;
}
if (way.hasTag("railway", "construction")) {
isUnderConstruction = true;
}
} else if (relation.hasTag("route", "train")) {
if (!way.hasTag("railway", "rail")) {
isCorrectRoadType = false;
}
if (way.hasTag("railway", "construction")) {
isUnderConstruction = true;
}
}
if (!isCorrectRoadType && !isUnderConstruction) {
List<Relation> primitives = new ArrayList<>(1);
primitives.add(relation);
List<Way> highlighted = new ArrayList<>(1);
highlighted.add(way);
Builder builder = TestError.builder(this.test, Severity.WARNING, PTAssistantValidatorTest.ERROR_CODE_ROAD_TYPE);
builder.message(tr("PT: Route type does not match the type of the road it passes on"));
builder.primitives(primitives);
builder.highlight(highlighted);
TestError e = builder.build();
errors.add(e);
}
if (isUnderConstruction) {
List<Relation> primitives = new ArrayList<>(1);
primitives.add(relation);
List<Way> highlighted = new ArrayList<>(1);
highlighted.add(way);
Builder builder = TestError.builder(this.test, Severity.WARNING, PTAssistantValidatorTest.ERROR_CODE_CONSTRUCTION);
builder.message(tr("PT: Road is under construction"));
builder.primitives(primitives);
builder.highlight(highlighted);
TestError e = builder.build();
errors.add(e);
}
}
}
}
protected void performDirectionTest() {
List<Way> waysToCheck = new ArrayList<>();
for (RelationMember rm : relation.getMembers()) {
if (RouteUtils.isPTWay(rm)) {
if (rm.isWay()) {
waysToCheck.add(rm.getWay());
} else {
Relation nestedRelation = rm.getRelation();
for (RelationMember nestedRelationMember : nestedRelation.getMembers()) {
waysToCheck.add(nestedRelationMember.getWay());
}
}
}
}
if (waysToCheck.size() <= 1) {
return;
}
List<Way> problematicWays = new ArrayList<>();
for (int i = 0; i < waysToCheck.size(); i++) {
Way curr = waysToCheck.get(i);
if (i == 0) {
// first way:
Way next = waysToCheck.get(i + 1);
if (!touchCorrectly(null, curr, next)) {
problematicWays.add(curr);
}
} else if (i == waysToCheck.size() - 1) {
// last way:
Way prev = waysToCheck.get(i - 1);
if (!touchCorrectly(prev, curr, null)) {
problematicWays.add(curr);
}
} else {
// all other ways:
Way prev = waysToCheck.get(i - 1);
Way next = waysToCheck.get(i + 1);
if (!touchCorrectly(prev, curr, next)) {
problematicWays.add(curr);
}
}
}
List<Relation> primitives = new ArrayList<>(1);
primitives.add(this.relation);
List<Set<Way>> listOfSets = new ArrayList<>();
for (Way problematicWay : problematicWays) {
Set<Way> primitivesToReport = new HashSet<>();
primitivesToReport.add(problematicWay);
primitivesToReport.addAll(checkAdjacentWays(problematicWay, new HashSet<Way>()));
listOfSets.add(primitivesToReport);
}
boolean changed = true;
while (changed) {
changed = false;
for (int i = 0; i < listOfSets.size(); i++) {
for (int j = i; j < listOfSets.size(); j++) {
if (i != j && RouteUtils.waysTouch(listOfSets.get(i), listOfSets.get(j))) {
listOfSets.get(i).addAll(listOfSets.get(j));
listOfSets.remove(j);
j = listOfSets.size();
changed = true;
}
}
}
}
for (Set<Way> currentSet : listOfSets) {
Builder builder = TestError.builder(this.test, Severity.WARNING, PTAssistantValidatorTest.ERROR_CODE_DIRECTION);
builder.message(tr("PT: Route passes a oneway road in the wrong direction"));
builder.primitives(primitives);
builder.highlight(currentSet);
TestError e = builder.build();
this.errors.add(e);
}
}
/**
* Checks if the current way touches its neighboring ways correctly
*
* @param prev
* can be null
* @param curr
* cannot be null
* @param next
* can be null
* @return {@code true} if the current way touches its neighboring ways correctly
*/
private boolean touchCorrectly(Way prev, Way curr, Way next) {
if (RouteUtils.isOnewayForPublicTransport(curr) == 0) {
return true;
}
if (prev != null) {
if (RouteUtils.waysTouch(curr, prev)) {
Node nodeInQuestion;
if (RouteUtils.isOnewayForPublicTransport(curr) == 1) {
nodeInQuestion = curr.firstNode();
} else {
nodeInQuestion = curr.lastNode();
}
List<Way> nb = findNeighborWays(curr, nodeInQuestion);
if (nb.size() < 2 && nodeInQuestion != prev.firstNode() && nodeInQuestion != prev.lastNode()) {
return false;
}
}
}
if (next != null) {
if (RouteUtils.waysTouch(curr, next)) {
Node nodeInQuestion;
if (RouteUtils.isOnewayForPublicTransport(curr) == 1) {
nodeInQuestion = curr.lastNode();
} else {
nodeInQuestion = curr.firstNode();
}
List<Way> nb = findNeighborWays(curr, nodeInQuestion);
if (nb.size() < 2 && nodeInQuestion != next.firstNode() && nodeInQuestion != next.lastNode()) {
return false;
}
}
}
return true;
}
protected Set<Way> checkAdjacentWays(Way curr, Set<Way> flags) {
// curr is supposed to be a wrong oneway way!!
Set<Way> resultSet = new HashSet<>();
resultSet.addAll(flags);
resultSet.add(curr);
if (RouteUtils.isOnewayForPublicTransport(curr) == 0) {
return null;
}
Node firstNodeInRouteDirection;
Node lastNodeInRouteDirection;
if (RouteUtils.isOnewayForPublicTransport(curr) == 1) {
firstNodeInRouteDirection = curr.lastNode();
lastNodeInRouteDirection = curr.firstNode();
} else {
firstNodeInRouteDirection = curr.firstNode();
lastNodeInRouteDirection = curr.lastNode();
}
List<Way> firstNodeInRouteDirectionNeighbors = findNeighborWays(curr, firstNodeInRouteDirection);
List<Way> lastNodeInRouteDirectionNeighbors = findNeighborWays(curr, lastNodeInRouteDirection);
for (Way nb : firstNodeInRouteDirectionNeighbors) {
if (resultSet.contains(nb)) {
continue;
}
if (RouteUtils.isOnewayForPublicTransport(nb) == 1 && nb.firstNode() == firstNodeInRouteDirection) {
Set<Way> newSet = this.checkAdjacentWays(nb, resultSet);
resultSet.addAll(newSet);
} else if (RouteUtils.isOnewayForPublicTransport(nb) == -1 && nb.lastNode() == firstNodeInRouteDirection) {
Set<Way> newSet = this.checkAdjacentWays(nb, resultSet);
resultSet.addAll(newSet);
}
}
for (Way nb : lastNodeInRouteDirectionNeighbors) {
if (resultSet.contains(nb)) {
continue;
}
if (RouteUtils.isOnewayForPublicTransport(nb) == 1 && nb.lastNode() == lastNodeInRouteDirection) {
Set<Way> newSet = this.checkAdjacentWays(nb, resultSet);
resultSet.addAll(newSet);
} else if (RouteUtils.isOnewayForPublicTransport(nb) == -1 && nb.firstNode() == lastNodeInRouteDirection) {
Set<Way> newSet = this.checkAdjacentWays(nb, resultSet);
resultSet.addAll(newSet);
}
}
return resultSet;
}
/**
* Finds all ways that touch the given way at the given node AND belong to
* the relation of this WayChecker
*
* @param way way
* @param node node
* @return all ways that touch the given way at the given node AND belong to
* the relation of this WayChecker
*/
private List<Way> findNeighborWays(Way way, Node node) {
List<Way> resultList = new ArrayList<>();
if (node != null) {
List<OsmPrimitive> nodeReferrers = node.getReferrers();
for (OsmPrimitive referrer : nodeReferrers) {
if (referrer.getType().equals(OsmPrimitiveType.WAY)) {
Way neighborWay = (Way) referrer;
if (neighborWay != way && containsWay(neighborWay)) {
resultList.add(neighborWay);
}
}
}
}
return resultList;
}
/**
* Checks if the relation of this WayChecker contains the given way
*
* @param way way
* @return {@code true} if the relation of this WayChecker contains the given way
*/
private boolean containsWay(Way way) {
List<RelationMember> members = relation.getMembers();
for (RelationMember rm : members) {
if (rm.isWay() && rm.getWay() == way) {
return true;
}
}
return false;
}
protected static Command fixErrorByRemovingWay(TestError testError) {
if (testError.getCode() != PTAssistantValidatorTest.ERROR_CODE_ROAD_TYPE
&& testError.getCode() != PTAssistantValidatorTest.ERROR_CODE_DIRECTION) {
return null;
}
Collection<? extends OsmPrimitive> primitives = testError.getPrimitives();
Relation originalRelation = (Relation) primitives.iterator().next();
Collection<?> highlighted = testError.getHighlighted();
Way wayToRemove = (Way) highlighted.iterator().next();
Relation modifiedRelation = new Relation(originalRelation);
List<RelationMember> modifiedRelationMembers = new ArrayList<>(originalRelation.getMembersCount() - 1);
// copy PT stops first, PT ways last:
for (RelationMember rm : originalRelation.getMembers()) {
if (RouteUtils.isPTStop(rm)) {
if (rm.getRole().equals("stop_position")) {
if (rm.getType().equals(OsmPrimitiveType.NODE)) {
RelationMember newMember = new RelationMember("stop", rm.getNode());
modifiedRelationMembers.add(newMember);
} else { // if it is a way:
RelationMember newMember = new RelationMember("stop", rm.getWay());
modifiedRelationMembers.add(newMember);
}
} else {
// if the relation member does not have the role
// "stop_position":
modifiedRelationMembers.add(rm);
}
}
}
// now copy PT ways:
for (RelationMember rm : originalRelation.getMembers()) {
if (RouteUtils.isPTWay(rm)) {
Way wayToCheck = rm.getWay();
if (wayToCheck != wayToRemove) {
if (rm.getRole().equals("forward") || rm.getRole().equals("backward")) {
RelationMember modifiedMember = new RelationMember("", wayToCheck);
modifiedRelationMembers.add(modifiedMember);
} else {
modifiedRelationMembers.add(rm);
}
}
}
}
modifiedRelation.setMembers(modifiedRelationMembers);
ChangeCommand changeCommand = new ChangeCommand(originalRelation, modifiedRelation);
return changeCommand;
}
}