/*
* 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());
}
}