// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.turnlanes.model; import static org.openstreetmap.josm.tools.I18n.tr; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.openstreetmap.josm.data.osm.DataSet; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; 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.turnlanes.model.Issue.QuickFix; public class Validator { private static final class IncomingLanes { private static final class Key { final Node junction; final Way from; Key(Node junction, Way from) { this.junction = junction; this.from = from; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((from == null) ? 0 : from.hashCode()); result = prime * result + ((junction == null) ? 0 : junction.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Key other = (Key) obj; if (from == null) { if (other.from != null) return false; } else if (!from.equals(other.from)) return false; if (junction == null) { if (other.junction != null) return false; } else if (!junction.equals(other.junction)) return false; return true; } } final Key key; private final int extraLeft; private final int regular; private final int extraRight; private final BitSet bitset; IncomingLanes(Key key, int extraLeft, int regular, int extraRight) { this.key = key; this.extraLeft = extraLeft; this.regular = regular; this.extraRight = extraRight; this.bitset = new BitSet(extraLeft + regular + extraRight); } public boolean existsRegular(int l) { if (l > 0 && l <= regular) { bitset.set(extraLeft + l - 1); return true; } return false; } public boolean existsExtra(int l) { if (l < 0 && Math.abs(l) <= extraLeft) { bitset.set(Math.abs(l) - 1); return true; } else if (l > 0 && l <= extraRight) { bitset.set(extraLeft + regular + l - 1); return true; } return false; } public int unreferenced() { return extraLeft + regular + extraRight - bitset.cardinality(); } } public List<Issue> validate(DataSet dataSet) { final List<Relation> lenghts = new ArrayList<>(); final List<Relation> turns = new ArrayList<>(); for (Relation r : OsmPrimitive.getFilteredList(dataSet.allPrimitives(), Relation.class)) { if (!r.isUsable()) { continue; } final String type = r.get("type"); if (Constants.TYPE_LENGTHS.equals(type)) { lenghts.add(r); } else if (Constants.TYPE_TURNS.equals(type)) { turns.add(r); } } final List<Issue> issues = new ArrayList<>(); final Map<IncomingLanes.Key, IncomingLanes> incomingLanes = new HashMap<>(); issues.addAll(validateLengths(lenghts, incomingLanes)); issues.addAll(validateTurns(turns, incomingLanes)); for (IncomingLanes lanes : incomingLanes.values()) { if (lanes.unreferenced() > 0) { issues.add(Issue.newWarning(Arrays.asList(lanes.key.junction, lanes.key.from), tr("{0} lanes are not referenced in any turn-relation.", lanes.unreferenced()))); } } return issues; } private List<Issue> validateLengths(List<Relation> lenghts, Map<IncomingLanes.Key, IncomingLanes> incomingLanes) { final List<Issue> issues = new ArrayList<>(); for (Relation r : lenghts) { issues.addAll(validateLengths(r, incomingLanes)); } return issues; } private List<Issue> validateLengths(Relation r, Map<IncomingLanes.Key, IncomingLanes> incomingLanes) { final List<Issue> issues = new ArrayList<>(); try { final Node end = Utils.getMemberNode(r, Constants.LENGTHS_ROLE_END); final Route route = validateLengthsWays(r, end, issues); if (route == null) { return issues; } final List<Double> left = Lane.loadLengths(r, Constants.LENGTHS_KEY_LENGTHS_LEFT, 0); final List<Double> right = Lane.loadLengths(r, Constants.LENGTHS_KEY_LENGTHS_RIGHT, 0); int tooLong = 0; for (Double l : left) { if (l > route.getLength()) { ++tooLong; } } for (Double l : right) { if (l > route.getLength()) { ++tooLong; } } if (tooLong > 0) { issues.add(Issue.newError(r, end, "The lengths-relation specifies " + tooLong + " extra-lanes which are longer than its ways.")); } putIncomingLanes(route, left, right, incomingLanes); return issues; } catch (UnexpectedDataException e) { issues.add(Issue.newError(r, e.getMessage())); return issues; } } private void putIncomingLanes(Route route, List<Double> left, List<Double> right, Map<IncomingLanes.Key, IncomingLanes> incomingLanes) { final Node end = route.getLastSegment().getEnd(); final Way way = route.getLastSegment().getWay(); final IncomingLanes.Key key = new IncomingLanes.Key(end, way); final IncomingLanes lanes = new IncomingLanes(key, left.size(), Lane.getRegularCount(way, end), right.size()); final IncomingLanes old = incomingLanes.put(key, lanes); if (old != null) { incomingLanes.put( key, new IncomingLanes(key, Math.max(lanes.extraLeft, old.extraLeft), Math.max(lanes.regular, old.regular), Math.max(lanes.extraRight, old.extraRight))); } } private Route validateLengthsWays(Relation r, Node end, List<Issue> issues) { final List<Way> ways = Utils.getMemberWays(r, Constants.LENGTHS_ROLE_WAYS); if (ways.isEmpty()) { issues.add(Issue.newError(r, "A lengths-relation requires at least one member-way with role \"" + Constants.LENGTHS_ROLE_WAYS + "\".")); return null; } Node current = end; for (Way w : ways) { if (!w.isFirstLastNode(current)) { return orderWays(r, ways, current, issues, "ways", "lengths"); } current = Utils.getOppositeEnd(w, current); } return Route.create(ways, end); } private Route orderWays(final Relation r, List<Way> ways, Node end, List<Issue> issues, String role, String type) { final List<Way> unordered = new ArrayList<>(ways); final List<Way> ordered = new ArrayList<>(ways.size()); final Set<Node> ends = new HashSet<>(); // to find cycles Node current = end; findNext: while (!unordered.isEmpty()) { if (!ends.add(current)) { issues.add(Issue.newError(r, ways, "The " + role + " of the " + type + "-relation are unordered (and contain cycles).")); return null; } Iterator<Way> it = unordered.iterator(); while (it.hasNext()) { final Way w = it.next(); if (w.isFirstLastNode(current)) { it.remove(); ordered.add(w); current = Utils.getOppositeEnd(w, current); continue findNext; } } issues.add(Issue.newError(r, ways, "The " + role + " of the " + type + "-relation are disconnected.")); return null; } final QuickFix quickFix = new QuickFix(tr("Put the ways in order.")) { @Override public boolean perform() { for (int i = r.getMembersCount() - 1; i >= 0; --i) { final RelationMember m = r.getMember(i); if (m.isWay() && Constants.LENGTHS_ROLE_WAYS.equals(m.getRole())) { r.removeMember(i); } } for (Way w : ordered) { r.addMember(new RelationMember(Constants.LENGTHS_ROLE_WAYS, w)); } return true; } }; issues.add(Issue.newError(r, ways, "The ways of the lengths-relation are unordered.", quickFix)); return Route.create(ordered, end); } private List<Issue> validateTurns(List<Relation> turns, Map<IncomingLanes.Key, IncomingLanes> incomingLanes) { final List<Issue> issues = new ArrayList<>(); for (Relation r : turns) { issues.addAll(validateTurns(r, incomingLanes)); } return issues; } private List<Issue> validateTurns(Relation r, Map<IncomingLanes.Key, IncomingLanes> incomingLanes) { final List<Issue> issues = new ArrayList<>(); try { final Way from = Utils.getMemberWay(r, Constants.TURN_ROLE_FROM); final Way to = Utils.getMemberWay(r, Constants.TURN_ROLE_TO); if (from.firstNode().equals(from.lastNode())) { issues.add(Issue.newError(r, from, "The from-way both starts as well as ends at the via-node.")); } if (to.firstNode().equals(to.lastNode())) { issues.add(Issue.newError(r, to, "The to-way both starts as well as ends at the via-node.")); } if (!issues.isEmpty()) { return issues; } final Node fromJunctionNode; final List<RelationMember> viaMembers = Utils.getMembers(r, Constants.TURN_ROLE_VIA); if (viaMembers.isEmpty()) { throw UnexpectedDataException.Kind.NO_MEMBER.chuck(Constants.TURN_ROLE_VIA); } else if (viaMembers.get(0).isWay()) { final List<Way> vias = Utils.getMemberWays(r, Constants.TURN_ROLE_VIA); fromJunctionNode = Utils.lineUp(from, vias.get(0)); Node current = fromJunctionNode; for (Way via : vias) { if (!via.isFirstLastNode(current)) { orderWays(r, vias, current, issues, "via-ways", "turns"); break; } current = Utils.getOppositeEnd(via, current); } } else { final Node via = Utils.getMemberNode(r, Constants.TURN_ROLE_VIA); if (!from.isFirstLastNode(via)) { issues.add(Issue.newError(r, from, "The from-way does not start or end at the via-node.")); } if (!to.isFirstLastNode(via)) { issues.add(Issue.newError(r, to, "The to-way does not start or end at the via-node.")); } fromJunctionNode = via; } if (!issues.isEmpty()) { return issues; } final IncomingLanes lanes = get(incomingLanes, fromJunctionNode, from); for (int l : splitInts(r, Constants.TURN_KEY_LANES, issues)) { if (!lanes.existsRegular(l)) { issues.add(Issue.newError(r, tr("Relation references non-existent (regular) lane {0}", l))); } } for (int l : splitInts(r, Constants.TURN_KEY_EXTRA_LANES, issues)) { if (!lanes.existsExtra(l)) { issues.add(Issue.newError(r, tr("Relation references non-existent extra lane {0}", l))); } } return issues; } catch (UnexpectedDataException e) { issues.add(Issue.newError(r, e.getMessage())); return issues; } } private List<Integer> splitInts(Relation r, String key, List<Issue> issues) { final String ints = r.get(key); if (ints == null) { return Collections.emptyList(); } final List<Integer> result = new ArrayList<>(); for (String s : Constants.SPLIT_PATTERN.split(ints)) { try { int i = Integer.parseInt(s.trim()); result.add(Integer.valueOf(i)); } catch (NumberFormatException e) { issues.add(Issue.newError(r, tr("Integer list \"{0}\" contains unexpected values.", key))); } } return result; } private IncomingLanes get(Map<IncomingLanes.Key, IncomingLanes> incomingLanes, Node via, Way from) { final IncomingLanes.Key key = new IncomingLanes.Key(via, from); final IncomingLanes lanes = incomingLanes.get(key); if (lanes == null) { final IncomingLanes newLanes = new IncomingLanes(key, 0, Lane.getRegularCount(from, via), 0); incomingLanes.put(key, newLanes); return newLanes; } else { return lanes; } } }