/* * Copyright (C) 2013. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 or * version 2 as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. */ package uk.me.parabola.mkgmap.reader.osm; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import uk.me.parabola.imgfmt.app.Area; import uk.me.parabola.imgfmt.app.Coord; import uk.me.parabola.imgfmt.app.CoordNode; import uk.me.parabola.imgfmt.app.net.AccessTagsAndBits; import uk.me.parabola.imgfmt.app.net.GeneralRouteRestriction; import uk.me.parabola.log.Logger; import uk.me.parabola.mkgmap.general.MapCollector; /** * Representation of an OSM turn restriction * * @author Mark Burton, GerdP */ public class RestrictionRelation extends Relation { private static final Logger log = Logger.getLogger(RestrictionRelation.class); private List<Long> fromWayIds = new ArrayList<>(2); private List<Long> toWayIds = new ArrayList<>(2); private List<Long> viaWayIds = new ArrayList<>(2); private List<Coord> viaPoints = new ArrayList<>(2); private HashSet<Long> updatedViaWays = new HashSet<>(); private Coord viaCoord; private String restriction; private byte exceptMask; private char dirIndicator; private String messagePrefix; private boolean valid; private boolean evalWasCalled; // These tags are not loaded by default but if they exist issue a warning private final static String[] unsupportedTags = { "day_on", "day_off", "hour_on", "hour_off" }; private final static byte DEFAULT_EXCEPT_MASK = AccessTagsAndBits.FOOT | AccessTagsAndBits.EMERGENCY; private final static List<String> supportedRestrictions = Arrays.asList( "no_right_turn", "no_left_turn", "no_u_turn", "no_straight_on", "only_right_turn", "only_left_turn", "only_straight_on", "no_entry", "no_exit" ); /** * Create an instance based on an existing relation. We need to do * this because the type of the relation is not known until after all * its tags are read in. * @param other The relation to base this one on. */ public RestrictionRelation(Relation other) { setId(other.getId()); messagePrefix = "Turn restriction " + toBrowseURL(); copyTags(other); for (Map.Entry<String, Element> pair : other.getElements()) { addElement(pair.getKey(), pair.getValue()); } } /** * The evaluation should happen after style processing. * Normally this is called from the {@link RelationStyleHook} * Performs also diverse plausibility checks. * @param bbox tile boundary */ public void eval(Area bbox){ if (evalWasCalled){ log.error(messagePrefix,"internal error: eval() was already called"); fromWayIds.clear(); toWayIds.clear(); viaWayIds.clear(); } evalWasCalled = true; if (getTag("type") == null){ // style removed the tag log.info(messagePrefix, "type tag was removed, relation is ignored"); valid = false; return; } List<Way> fromWays = new ArrayList<>(); List<Way> toWays = new ArrayList<>(); List<Way> viaWays = new ArrayList<>(); final String browseURL = toBrowseURL(); valid = true; // find out what kind of restriction we have and to which vehicles it applies exceptMask = DEFAULT_EXCEPT_MASK; String specifc_type = getTag("restriction"); int count_unknown = 0; Map<String, String> vehicles = getTagsWithPrefix("restriction:", true); if (vehicles.isEmpty() == false){ exceptMask = (byte) 0xff; Iterator<Entry<String, String>> iter = vehicles.entrySet().iterator(); while (iter.hasNext()){ Map.Entry<String, String> entry = iter.next(); String vehicle = entry.getKey(); if (setExceptMask(vehicle, false) == false) count_unknown++; if (specifc_type == null) specifc_type = entry.getValue(); else if (specifc_type.equals(entry.getValue()) == false){ log.warn(messagePrefix, "is invalid, it specifies different kinds of turns"); valid = false; break; } } if (valid && vehicles.size() == count_unknown){ log.warn(messagePrefix, "no supported vehicle in turn restriction"); valid = false; return; } } if (specifc_type == null){ // style removed the tag log.info(messagePrefix, "no valid restriction tag found"); valid = false; return; } restriction = specifc_type.trim(); messagePrefix = "Turn restriction (" + restriction + ") " + browseURL; if (supportedRestrictions.contains(restriction) == false){ log.warn(messagePrefix, "ignoring unsupported restriction type '" + restriction + "'"); valid = false; return; } String dirInfo = ""; if (restriction.contains("left")) dirInfo += "l"; if (restriction.contains("right")) dirInfo += "r"; if (restriction.contains("straight")) dirInfo += "s"; if (restriction.endsWith("u_turn")) dirInfo += "u"; if (dirInfo.length() > 1){ log.warn(messagePrefix, "ignoring unsupported restriction type '" + restriction + "'"); valid = false; return; } else if (dirInfo.length() == 1){ dirIndicator = dirInfo.charAt(0); } else dirIndicator = '?'; String type = getTag("type"); if (type.startsWith("restriction:")){ exceptMask = (byte) 0xff; String vehicle = type.substring("restriction:".length()); if (setExceptMask(vehicle, false) == false) { log.warn(messagePrefix, "ignoring unsupported '" + vehicle + "' in turn restriction"); valid = false; return; } } String except = getTag("except"); if(except != null) { for(String vehicle : except.split("[,;]")) { // be nice vehicle = vehicle.trim(); setExceptMask(vehicle, true); } } for (String unsupportedTag : unsupportedTags) { if (getTag(unsupportedTag) != null) { log.warn(messagePrefix, "ignoring unsupported '" + unsupportedTag + "' tag"); } } // evaluate members for (Map.Entry<String, Element> pair : getElements()) { String role = pair.getKey(); Element el = pair.getValue(); Coord location = null; if(viaCoord != null) location = viaCoord; else if(!fromWays.isEmpty() && !fromWays.get(0).getPoints().isEmpty()) location = fromWays.get(0).getPoints().get(0); else if(!toWays.isEmpty() && !toWays.get(0).getPoints().isEmpty()) location = toWays.get(0).getPoints().get(0); else if(!viaWays.isEmpty() && !viaWays.get(0).getPoints().isEmpty()) location = viaWays.get(0).getPoints().get(0); if(location != null) messagePrefix = "Turn restriction (" + restriction + ") " + browseURL + " (at " + location.toOSMURL() + ")"; if("to".equals(role)) { if(!(el instanceof Way)) { log.warn(messagePrefix, "'to' member", el.toBrowseURL(), "is not a way but it should be"); } else if(((Way)el).getPoints().isEmpty()) { log.warn(messagePrefix, "ignoring empty 'to' way", el.toBrowseURL()); } else toWays.add((Way)el); } else if("from".equals(role)) { if(!(el instanceof Way)) { log.warn(messagePrefix, "'from' member", el.toBrowseURL(), "is not a way but it should be"); } else if(((Way)el).getPoints().isEmpty()) { log.warn(messagePrefix, "ignoring empty 'from' way", el.toBrowseURL()); } else fromWays.add((Way)el); } else if("via".equals(role)) { if(el instanceof Node) { if (viaCoord != null){ log.warn(messagePrefix, "has extra 'via' node", el.toBrowseURL()); valid = false; } else viaCoord = ((Node)el).getLocation(); } else if(el instanceof Way) { if (viaCoord != null){ log.warn(messagePrefix, "has extra 'via' way", el.toBrowseURL()); valid = false; } else viaWays.add((Way)el); } else { log.warn(messagePrefix, "'via' member", el.toBrowseURL(), "is not a node or way"); } } else if("location_hint".equals(role)) { // relax - we don't care about this } else { log.warn(messagePrefix, "unknown member role '" + role + "'"); } } if (!valid) return; if ("no_entry".equals(restriction) == false){ if (fromWays.size() > 1){ log.warn(messagePrefix, "multiple 'from' members are only accepted for no_entry restrictions"); valid = false; return; } } if ("no_exit".equals(restriction) == false){ if (toWays.size() > 1){ log.warn(messagePrefix, "multiple 'to' members are only accepted for no_exit restrictions"); valid = false; return; } } if (viaWays.isEmpty() && viaCoord == null && fromWays.size() == 1 && toWays.size() == 1){ Way fromWay = fromWays.get(0); Way toWay = toWays.get(0); List<Coord>fromPoints = fromWay.getPoints(); List<Coord>toPoints = toWay.getPoints(); int countSame = 0; for(Coord fp : fromPoints) { for(Coord tp : toPoints) { if(fp == tp){ countSame++; viaCoord = fp; } } } if (countSame > 1){ log.warn(messagePrefix, "lacks 'via' node and way and the 'from' (", fromWay.toBrowseURL(), ") and 'to' (", toWay.toBrowseURL(), ") ways connect in more than one place"); valid = false; } else if (viaCoord == null){ log.warn(messagePrefix, "lacks 'via' node and the 'from' (" + fromWay.toBrowseURL() + ") and 'to' (" + toWay.toBrowseURL() + ") ways don't connect"); valid = false; } else { if (fromPoints.get(0) != viaCoord && fromPoints.get(fromPoints.size()-1) != viaCoord || toPoints.get(0) != viaCoord && toPoints.get(toPoints.size()-1) != viaCoord){ log.warn(messagePrefix, "lacks 'via' node and the 'from' (" + fromWay.toBrowseURL() + ") and 'to' (" + toWay.toBrowseURL() + ") ways don't connect at an end point"); valid = false; } else log.warn(messagePrefix, "lacks 'via' node (guessing it should be at", viaCoord.toOSMURL() + ", why don't you add it to the OSM data?)"); } } if(fromWays.isEmpty() ) { log.warn(messagePrefix, "lacks 'from' way"); valid = false; } if(toWays.isEmpty()) { log.warn(messagePrefix, "lacks 'to' way"); valid = false; } if ((fromWays.size() > 1 || toWays.size() > 1) && viaWays.isEmpty() == false){ log.warn(messagePrefix, "'via' way(s) are not supported with multiple 'from' or 'to' ways"); valid = false; } if (!valid) return; for (List<Way> ways : Arrays.asList(fromWays,viaWays,toWays)){ for (Way way : ways){ if (way.getPoints().size() < 2){ log.warn(messagePrefix,"way",way.toBrowseURL(),"has less than 2 points, restriction is ignored"); valid = false; } else { if (way.getPoints().get(0) == way.getPoints().get(way.getPoints().size()-1)){ if (ways == toWays && dirIndicator != '?') continue; // we try to determine the correct part in RoadNetwork log.warn(messagePrefix, "way", way.toBrowseURL(), "starts and ends at same node, don't know which one to use"); valid = false; } } } } if (!valid) return; if (viaPoints.isEmpty() == false) viaCoord = viaPoints.get(0); if(viaCoord == null && viaWays.isEmpty()) { valid = false; return; } viaPoints.clear(); Coord v1 = viaCoord; Coord v2 = viaCoord; if (viaWays.isEmpty() == false){ v1 = viaWays.get(0).getPoints().get(0); v2 = viaWays.get(0).getPoints().get(viaWays.get(0).getPoints().size()-1); } // check if all from ways are connected at the given via point or with the given via ways for (Way fromWay : fromWays){ Coord e1 = fromWay.getPoints().get(0); Coord e2 = fromWay.getPoints().get(fromWay.getPoints().size() - 1); if (e1 == v1 || e2 == v1) viaCoord = v1; else if (e1 == v2 || e2 == v2) viaCoord = v2; else { log.warn(messagePrefix, "'from' way", fromWay.toBrowseURL(), "doesn't start or end at 'via' node or way"); valid = false; } } if (!valid) return; viaPoints.add(viaCoord); // check if via ways are connected in the given order for (int i = 0; i < viaWays.size();i++){ Way way = viaWays.get(i); Coord v = viaPoints.get(viaPoints.size()-1); if (way.getPoints().get(0) == v) v2 = way.getPoints().get(way.getPoints().size()-1); else if (way.getPoints().get(way.getPoints().size()-1) == v) v2 = way.getPoints().get(0); else { log.warn(messagePrefix, "'via' way", way.toBrowseURL(), "doesn't start or end at",v.toDegreeString()); valid = false; } viaPoints.add(v2); } // check if all via points are inside the bounding box int countInside = 0; for (Coord via: viaPoints){ if(bbox.contains(via)) ++countInside; } if (countInside == 0) valid = false; else if (countInside > 0 && countInside < viaPoints.size()){ log.warn(messagePrefix,"via way crosses tile boundary. Don't know how to save that, ignoring it"); valid = false; } if (!valid) return; // check if all to ways are connected to via point or last via way Coord lastVia = viaPoints.get(viaPoints.size()-1); for (Way toWay : toWays){ Coord e1 = toWay.getPoints().get(0); Coord e2 = toWay.getPoints().get(toWay.getPoints().size() - 1); if(e1 != lastVia && e2 != lastVia) { log.warn(messagePrefix, "'to' way", toWay.toBrowseURL(), "doesn't start or end at 'via' node or way"); valid = false; } } if (valid && !viaWays.isEmpty() && restriction.startsWith("only")){ log.warn(messagePrefix, "check: 'via' way(s) are used in",restriction,"restriction"); } if (valid){ // make sure that via way(s) don't appear in the from or to lists for (Way w: viaWays){ if (fromWays.contains(w)){ log.warn(messagePrefix, "'via' way",w.toBrowseURL(),"appears also as 'from' way"); valid = false; } if (toWays.contains(w)){ log.warn(messagePrefix, "'via' way",w.toBrowseURL(),"appears also as 'to' way"); valid = false; } } } if (valid){ for (Way w: fromWays) fromWayIds.add(w.getId()); for (Way w: toWays) toWayIds.add(w.getId()); for (Way w: viaWays){ w.setViaWay(true); viaWayIds.add(w.getId()); } for (Coord v: viaPoints) v.setViaNodeOfRestriction(true); } } /** * Match the vehicle type in a restriction with the mkgmap type * and modify the exceptMask * @param vehicle * @param b true: restriction should not apply for vehicle, false: restriction should apply * @return true if vehicle has a matching flag in the garmin format */ private boolean setExceptMask(String vehicle, boolean b){ byte flag = 0; if (vehicle == null) return false; // inverted if(vehicle.equals("vehicle")) flag = (byte) ~(DEFAULT_EXCEPT_MASK); else if(vehicle.equals("motor_vehicle")) flag = (byte) ~(AccessTagsAndBits.BIKE | DEFAULT_EXCEPT_MASK); // normal else if(vehicle.equals("psv")) flag = (byte) (AccessTagsAndBits.TAXI | AccessTagsAndBits.BUS); else if(vehicle.equals("bicycle")) flag = AccessTagsAndBits.BIKE; else if(vehicle.equals("motorcar")) flag = AccessTagsAndBits.CAR; else if(vehicle.equals("bus")) flag = AccessTagsAndBits.BUS; else if(vehicle.equals("taxi")) flag = AccessTagsAndBits.TAXI; else if(vehicle.equals("goods")) flag = AccessTagsAndBits.DELIVERY; else if(vehicle.equals("hgv") || vehicle.equals("truck")) flag = AccessTagsAndBits.TRUCK; else if(vehicle.equals("emergency")) flag = AccessTagsAndBits.EMERGENCY; else if(vehicle.equals("foot")) flag = AccessTagsAndBits.FOOT; if (flag == 0){ log.warn(messagePrefix, "ignoring unsupported vehicle class '" + vehicle + "' in turn restriction"); return false; } if (b) exceptMask |= flag; else exceptMask &= ~flag; return true; } public boolean isFromWay(long wayId) { return fromWayIds.contains(wayId); } public boolean isToWay(long wayId) { return toWayIds.contains(wayId); } public void replaceViaCoord(Coord oldP, Coord newP) { for (int i = 0; i < viaPoints.size(); i++){ if (viaPoints.get(i) == oldP){ viaPoints.set(i, newP); if (log.isDebugEnabled()){ log.debug(messagePrefix, restriction, "'via' coord redefined from", oldP.toOSMURL(), "to", newP.toOSMURL()); } return; } } } public void addRestriction(MapCollector collector, IdentityHashMap<Coord, CoordNode> nodeIdMap) { if (!valid) return; List<CoordNode> viaNodes = new ArrayList<>(); for (Coord v: viaPoints){ CoordNode vn = nodeIdMap.get(v); if (vn == null){ log.error(messagePrefix,"via node is not a routing node"); return; } viaNodes.add(vn); } if (viaNodes.size() > 6){ log.warn(messagePrefix,"has more than 6 via nodes, this is not supported"); return; } if(restriction == null){ log.error("internal error: can't add valid restriction relation", this.getId(), "type", restriction); return; } int addedRestrictions = 0; GeneralRouteRestriction grr; if(restriction.startsWith("no_")){ for (long fromWayId : fromWayIds){ for (long toWayId : toWayIds){ grr = new GeneralRouteRestriction("not", exceptMask, messagePrefix); grr.setFromWayId(fromWayId); grr.setToWayId(toWayId); grr.setViaNodes(viaNodes); grr.setViaWayIds(viaWayIds); grr.setDirIndicator(dirIndicator); addedRestrictions += collector.addRestriction(grr); } } if (log.isInfoEnabled()) log.info(messagePrefix, restriction, "translated to",addedRestrictions,"img file restrictions"); } else if(restriction.startsWith("only_")){ grr = new GeneralRouteRestriction("only", exceptMask, messagePrefix); grr.setFromWayId(fromWayIds.get(0)); grr.setToWayId(toWayIds.get(0)); grr.setViaNodes(viaNodes); grr.setViaWayIds(viaWayIds); grr.setDirIndicator(dirIndicator); int numAdded = collector.addRestriction(grr); if (numAdded > 0) log.info(messagePrefix, restriction, "added - allows routing to way", toWayIds.get(0)); } else { log.error("mkgmap internal error: unknown restriction", restriction); } } /** Process the members in this relation. */ public void processElements() { // relax } public String toString() { String s = "[restriction id = " + getId() + "(" + restriction + ")"; if (!fromWayIds.isEmpty() && !toWayIds.isEmpty() && viaCoord != null ) s += ", from = " + fromWayIds.get(0) + ", to = " + toWayIds.get(0) + ", via = " + viaCoord.toOSMURL() + "]"; else s += "]"; return s; } /** * @return true if restriction is usable */ public boolean isValid() { assert evalWasCalled; return valid; } public List<Coord> getViaCoords() { assert evalWasCalled; return viaPoints; } /** * * @return a Set with the OSM IDs of all ways used in the restriction */ public Set<Long> getWayIds(){ assert evalWasCalled; Set<Long> wayIds = new HashSet<>(); wayIds.addAll(fromWayIds); wayIds.addAll(viaWayIds); wayIds.addAll(toWayIds); return wayIds; } public byte getExceptMask(){ assert evalWasCalled; return exceptMask; } /** * Replace * @param oldWayId * @param newWayId * @return */ public boolean replaceWay(long oldWayId, long newWayId) { assert evalWasCalled; boolean matched = false; for (List<Long> ways: Arrays.asList(fromWayIds, viaWayIds, toWayIds)){ for (int i = 0; i < ways.size(); i++){ if (ways.get(i) == oldWayId){ ways.set(i, newWayId); matched = true; } } } return matched; } /** * check if restriction is still valid if the way with the given id is not in the map * @param wayId * @return */ public boolean isValidWithoputWay(long wayId) { assert evalWasCalled; if (viaWayIds.contains(wayId)) return false; fromWayIds.remove(wayId); if (fromWayIds.isEmpty()) return false; // else it must be a no_entry restriction which still is valid toWayIds.remove(wayId); if (toWayIds.isEmpty()) return false; // else it must be a no_exit restriction which still is valid return true; } /** * A via way may be connected to other ways between the end points. * We have to create a complete path for that. * @param way * @param nodeIndices */ public void updateViaWay(Way way, List<Integer> nodeIndices) { if (!valid) return; if (viaWayIds.contains(way.getId()) == false) return; if(updatedViaWays.contains(way.getId())){ // we may get here when the style adds multiple routable ways for the // OSM way if (viaPoints.size() != nodeIndices.size()) valid = false; else { Iterator<Coord> iter = viaPoints.iterator(); for (int pos : nodeIndices){ if (iter.hasNext()){ if (way.getPoints().get(pos).equals(iter.next())) continue; } valid = false; break; } } if (!valid) log.error(messagePrefix, "internal error: via way is updated again with different nodes"); else { // already up to date return; } } Coord first = way.getPoints().get(nodeIndices.get(0)); Coord last = way.getPoints().get( nodeIndices.get(nodeIndices.size() - 1)); int posFirst = -1; int posLast = -1; for (int i = 0; i < viaPoints.size(); i++) { if (first == viaPoints.get(i)) posFirst = i; if (last== viaPoints.get(i)) posLast = i; if (posFirst >= 0 && posLast >= 0){ if (Math.abs(posLast - posFirst) == 1){ break; } else { // log.error(messagePrefix, "check self intersection!"); } } } if (posFirst < 0 || posLast < 0){ log.error(messagePrefix, "internal error: via way doesn't contain expected points"); valid = false; return; } if (Math.abs(posLast - posFirst) != 1){ log.error(messagePrefix, "internal error: via way doesn't contain points in expected position"); valid = false; return; } List<Coord> midPoints = new ArrayList<>(); for (int i = 1; i + 1 < nodeIndices.size(); i++) { midPoints.add(way.getPoints().get(nodeIndices.get(i))); } if (posFirst < posLast){ if (posLast - posFirst > 1) viaPoints.subList(posFirst+1, posLast).clear(); viaPoints.addAll(posFirst + 1, midPoints); } else { if (posFirst - posLast > 1) viaPoints.subList(posLast + 1, posFirst).clear(); Collections.reverse(midPoints); viaPoints.addAll(posLast + 1, midPoints); } int wayPos = viaWayIds.indexOf(way.getId()); while(viaWayIds.size() > wayPos + 1 && viaWayIds.get(wayPos+1) == way.getId()) viaWayIds.remove(wayPos); for (int i = 0; i < midPoints.size(); i++){ viaWayIds.add(wayPos+1, way.getId()); } if (viaPoints.size() != viaWayIds.size()+1){ log.error(messagePrefix,"internal error: number of via points and via ways no longer fits"); valid = false; } else if (viaPoints.size() > 6){ log.warn(messagePrefix,"has more than 6 via nodes, this is not supported"); valid = false; } updatedViaWays.add(way.getId()); } }