/*
* Copyright (C) 2012
*
* 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.AbstractMap;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.Set;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.build.LocatorUtil;
import uk.me.parabola.mkgmap.osmstyle.function.LengthFunction;
import uk.me.parabola.util.EnhancedProperties;
import uk.me.parabola.util.MultiHashMap;
/**
* Copies the destination tag from motorway_link and trunk_link ways to the
* first adjacent non link way so that the Garmin is able to display a valid
* destination.
* @author WanMil
*/
public class LinkDestinationHook extends OsmReadingHooksAdaptor {
private static final Logger log = Logger.getLogger(LinkDestinationHook.class);
private ElementSaver saver;
/** Maps which ways can be driven from a given Coord */
private IdentityHashMap<Coord, Set<Way>> adjacentWays = new IdentityHashMap<Coord, Set<Way>>();
/** Contains all _link ways that have to be processed */
private Map<Long, Way> destinationLinkWays = new LinkedHashMap<Long, Way>();
private final static Set<String> highwayTypes = new LinkedHashSet<String>(Arrays.asList(
"motorway", "trunk", "primary", "secondary", "tertiary",
"motorway_link", "trunk_link", "primary_link", "secondary_link", "tertiary_link"));
private HashSet<String> linkTypes = new HashSet<String>(Arrays.asList(
"motorway_link", "trunk_link", "primary_link", "secondary_link", "tertiary_link"));
/** Map way ids to its restriction relations so that the relations can easily be updated when the way is split. */
private MultiHashMap<Long, RestrictionRelation> restrictions = new MultiHashMap<>();
private List<String> nameTags;
/** Maps which nodes contains to which ways */
private IdentityHashMap<Coord, Set<Way>> wayNodes = new IdentityHashMap<Coord, Set<Way>>();
private boolean processDestinations;
private boolean processExits;
public boolean init(ElementSaver saver, EnhancedProperties props) {
this.saver = saver;
nameTags = LocatorUtil.getNameTags(props);
processDestinations = props.containsKey("process-destination");
processExits = props.containsKey("process-exits");
return processDestinations || processExits;
}
/**
* Fills the internal lists
*/
private void retrieveWays() {
// collect all ways tagged with highway
for (Way w : saver.getWays().values()) {
if (w.getPoints().size() < 2) {
// ignore one-node or zero-node ways
continue;
}
String highwayTag = w.getTag("highway");
if (highwayTag != null && highwayTypes.contains(highwayTag)) {
// the points of the way are kept so that it is easy to get
// the adjacent ways for a given _link way
String directedDestination = null;
String directedDestinationLanes = null;
String directionSuffix = null;
List<Coord> points;
if (isOnewayInDirection(w)) {
// oneway => don't need the last point because the
// way cannot be driven standing at the last point
points = w.getPoints().subList(0, w.getPoints().size() - 1);
directedDestination = w.getTag("destination:forward");
directedDestinationLanes = w.getTag("destination:lanes:forward");
directionSuffix = "forward";
} else if (isOnewayOppositeDirection(w)) {
// reverse oneway => don't need the first point because the
// way cannot be driven standing at the first point
points = w.getPoints().subList(1, w.getPoints().size());
directedDestination = w.getTag("destination:backward");
directedDestinationLanes = w.getTag("destination:lanes:backward");
directionSuffix = "backward";
} else {
points = w.getPoints();
}
for (Coord c : points) {
Set<Way> ways = adjacentWays.get(c);
if (ways == null) {
ways = new HashSet<Way>(4);
adjacentWays.put(c, ways);
}
ways.add(w);
}
registerPointsOfWay(w);
// if the way is a link way and has a destination tag
// put it the list of ways that have to be processed
if (linkTypes.contains(highwayTag)) {
String destSourceTagKey = "destination";
String destinationTag = w.getTag("destination");
if (destinationTag == null) {
// destination is not set
// => check if destination:lanes is without any lane specific information (no |)
destSourceTagKey = "destination:lanes";
String destLanesTag = w.getTag(destSourceTagKey);
if (destLanesTag == null && directedDestinationLanes != null){
destLanesTag = directedDestinationLanes;
destSourceTagKey += ":" + directionSuffix;
}
if (destLanesTag != null && destLanesTag.contains("|") == false) {
// the destination:lanes tag contains no | => no lane specific information
// use this tag as destination tag
destinationTag = destLanesTag;
}
if (destinationTag == null && directedDestination != null) {
// use the destination:forward or :backward value
destinationTag = directedDestination;
destSourceTagKey = "destination:" + directionSuffix;
}
if (destinationTag == null){
// try to use the destination:street value
destSourceTagKey = "destination:street";
destinationTag = w.getTag(destSourceTagKey);
}
}
if (destinationTag != null){
w.addTag("mkgmap:dest_hint_work", destinationTag);
if ("destination".equals(destSourceTagKey) == false){
if (log.isDebugEnabled()){
if (destSourceTagKey.startsWith("destination:lanes"))
log.debug("Use",destSourceTagKey,"as destination tag because there is one lane information only. Way ",w.getId(),w.toTagString());
else
log.debug("Use",destSourceTagKey,"as destination tag. Way ",w.getId(),w.toTagString());
}
}
destinationLinkWays.put(w.getId(), w);
}
}
}
}
// get all restriction relations
// eventually they must be modified if one of its ways is split
for (Relation rel : saver.getRelations().values()) {
if (rel instanceof RestrictionRelation) {
RestrictionRelation rrel = (RestrictionRelation) rel;
for (Long wayId : rrel.getWayIds())
restrictions.add(wayId, rrel);
}
}
}
/**
* Registers the points of the given way for the internal data structures.
* @param w a new way
*/
private void registerPointsOfWay(Way w) {
for (Coord c : w.getPoints()) {
Set<Way> ways = wayNodes.get(c);
if (ways == null) {
ways = new HashSet<Way>(4);
wayNodes.put(c, ways);
}
ways.add(w);
}
}
/**
* Removes the points in range from to to from the way and the internal data structures.
* @param w way
* @param from first point to remove
* @param to range end to remove (exclusive)
*/
private void removePointsFromWay(Way w, int from, int to) {
// first remove them from the wayNodes map
for (Coord c : w.getPoints().subList(from, to)) {
wayNodes.get(c).remove(w);
}
// second remove them from the way
w.getPoints().subList(from, to).clear();
}
/**
* Retrieves the name of the given element based on the name-tag-list option.
* @param e an OSM element
* @return the name or <code>null</code> if the element has no name
*/
private String getName(Element e) {
if (e.getName()!= null) {
return e.getName();
}
for (String nameTag : nameTags) {
String nameTagVal = e.getTag(nameTag);
if (nameTagVal != null) {
return nameTagVal;
}
}
return null;
}
/**
* Check all restriction relations and eventually update the relations to use
* the split way if appropriate.
*
* @param oldWay the original way
* @param newWay the split part of the old way
*/
private void changeWayIdInRelations(Way oldWay, Way newWay) {
List<RestrictionRelation> wayRestrictions = restrictions.get(oldWay.getId());
if (wayRestrictions.isEmpty()) {
return;
}
if (oldWay.isViaWay())
newWay.setViaWay(true);
// create a copy because original list may be modified within the loop
for (RestrictionRelation rr : new ArrayList<>(wayRestrictions)) {
Coord lastPointNewWay = newWay.getPoints().get(0);
List<Coord> viaCoords = rr.getViaCoords();
for (Coord via : viaCoords){
if (via == lastPointNewWay) {
if (rr.isToWay(oldWay.getId())) {
log.debug("Change to-way",oldWay.getId(),"to",newWay.getId(),"for relation",rr.getId(),"at",lastPointNewWay.toOSMURL());
rr.replaceWay(oldWay.getId(), newWay.getId());
restrictions.removeMapping(oldWay.getId(), rr);
restrictions.add(newWay.getId(), rr);
} else if (rr.isFromWay(oldWay.getId())){
log.debug("Change from-way",oldWay.getId(),"to",newWay.getId(),"for relation",rr.getId(),"at",lastPointNewWay.toOSMURL());
rr.replaceWay(oldWay.getId(), newWay.getId());
restrictions.removeMapping(oldWay.getId(), rr);
restrictions.add(newWay.getId(), rr);
}
}
}
}
}
/**
* Cuts off at least minLength meter of the given way and returns the cut off way tagged
* identical to the given way.
* @param w the way to be cut
* @param maxLength the cut off way is no longer than this value
* @return the cut off way or <code>null</code> if cutting not possible
*/
private Way cutoffWay(Way w, double cutLength, double maxLength, Coord c1, Coord c2) {
if (w.getPoints().size()<2) {
return null;
}
if (w.getPoints().size() >= 3) {
// try to use existing points - that does not deform the way
Coord firstPoint = w.getPoints().get(0);
Coord cutPoint = w.getPoints().get(1);
// check if the maxLength is not exceeded
double dist = firstPoint.distance(cutPoint);
if (dist <= maxLength) {
// create a new way with the first two points and identical tags
Way precedingWay = new Way(w.getOriginalId(), w.getPoints().subList(0, 1 + 1));
precedingWay.setFakeId();
precedingWay.copyTags(w);
saver.addWay(precedingWay);
// remove the points of the new way from the original way
removePointsFromWay(w, 0, 1);
registerPointsOfWay(precedingWay);
// check and update relations so that they use the new way if appropriate
changeWayIdInRelations(w, precedingWay);
log.debug("Cut way", w, "at existing point 1. New way:",
precedingWay);
// return the new way
return precedingWay;
} else {
log.debug("Cannot cut way", w,
"on existing nodes because the first distance is too big:", dist);
}
}
double startSegmentLength = 0;
Coord lastC = w.getPoints().get(0);
for (int i = 1; i < w.getPoints().size(); i++) {
Coord c = w.getPoints().get(i);
double segmentLength = lastC.distance(c);
if (startSegmentLength + segmentLength >= cutLength) {
double frac = (cutLength - startSegmentLength) / segmentLength;
// insert a new point at the minimum distance
Coord cConnection = lastC.makeBetweenPoint(c, frac);
if (c1 != null && c2 != null && cConnection != null) {
// test if the way using the new point still uses the same
// orientation to the main motorway
double oldAngle = getAngle(c1, c2, c);
double newAngle = getAngle(c1, c2, cConnection);
if (Math.signum(oldAngle) != Math.signum(newAngle)) {
double bestAngleDiff = 180.0d;
Coord bestCoord = cConnection;
for (Coord cNeighbour : getDirectNeighbours(cConnection)) {
double neighbourAngle = getAngle(c1, c2, cNeighbour);
if (Math.signum(oldAngle) == Math.signum(neighbourAngle) &&
Math.abs(oldAngle - neighbourAngle) < bestAngleDiff) {
bestAngleDiff = Math.abs(oldAngle - neighbourAngle);
bestCoord = cNeighbour;
}
}
if (log.isDebugEnabled()) {
log.debug("Changed orientation:", oldAngle, "to",
newAngle);
log.debug("on Link", w);
log.debug("Corrected coord ", cConnection, "to",
bestCoord);
}
cConnection = bestCoord;
}
}
// create the new way with identical tags
w.getPoints().add(i,cConnection);
Way precedingWay = new Way(w.getOriginalId(), new ArrayList<Coord>(w.getPoints().subList(0, i+1)));
precedingWay.setFakeId();
precedingWay.copyTags(w);
saver.addWay(precedingWay);
// remove the points of the new way from the old way
removePointsFromWay(w, 0, i);
registerPointsOfWay(precedingWay);
// check and update relations so that they use the new way if appropriate
changeWayIdInRelations(w, precedingWay);
// return the split way
return precedingWay;
}
lastC = c;
}
// way too short
return null;
}
/**
* Retrieve a list of all Coords that are the direct neighbours of
* the given Coord. A neighbours latitude and longitude does not differ
* more than one Garmin unit from the given Coord.
* @param c the Coord for which the neighbours should be retrieved.
* @return all neighbours of c
*/
private List<Coord> getDirectNeighbours(Coord c) {
List<Coord> neighbours = new ArrayList<Coord>(8);
for (int dLat = -1; dLat<2; dLat++) {
for (int dLon = -1; dLon < 2; dLon++) {
if (dLat == 0 && dLon == 0) {
continue;
}
neighbours.add(new Coord(c.getLatitude()+dLat, c.getLongitude()+dLon));//TODO: move to Coord class?
}
}
return neighbours;
}
/**
* Retrieves if the given node is tagged as motorway exit. So it must contain at least the tags
* highway=motorway_junction and one of the tags ref, name or exit_to.
* @param node the node to check
* @return <code>true</code> the node is a motorway exit, <code>false</code> the node is not a
* motorway exit
*/
private boolean isTaggedAsExit(Node node) {
if ("motorway_junction".equals(node.getTag("highway")) == false) {
return false;
}
return node.getTag("ref") != null ||
(getName(node) != null) ||
node.getTag("exit_to") != null;
}
/**
* Retrieve all nodes that are connected to the given node either in
* driving direction or reverse.
* @param node a coord
* @param drivingDirection <code>true</code> driving direction; <code>false</code> reverse direction
* @return a list of all coords an the connection ways
*/
private List<Entry<Coord, Way>> getNextNodes(Coord node, boolean drivingDirection) {
List<Entry<Coord, Way>> nextNodes = new ArrayList<Entry<Coord, Way>>();
Set<Way> connectedWays = wayNodes.get(node);
for (Way w : connectedWays) {
// get the index of the node
int index = w.getPoints().indexOf(node);
if (index < 0) {
// this should not happen
log.error("Cannot find node "+node+" in way "+w);
continue;
}
boolean oneWayDirection = isOnewayInDirection(w);
// calc the index of the next node
index += (drivingDirection ? 1 : -1) * (oneWayDirection ? 1 : -1);
if (index >= 0 && index < w.getPoints().size()) {
nextNodes.add(new AbstractMap.SimpleEntry<Coord, Way>(w.getPoints().get(index), w));
}
}
return nextNodes;
}
/**
* Cuts motorway_link and trunk_link ways into three parts to be able to get
* a hint on Garmin GPS. This happens if the the option process-exits is set
* and the way is connected to an exit node (highway=motorway_junction)
* and/or the option process-destination is set and the destination tag is
* set. The mid part way is tagged additionally with the following tags:
* <ul>
* <li>mkgmap:dest_hint=* (for destinations)</li>
* <li>mkgmap:exit_hint=true (for exits)</li>
* <li>mkgmap:exit_hint_ref: Tagged with the ref tag value of the motorway
* junction node</li>
* <li>mkgmap:exit_hint_exit_to: Tagged with the exit_to tag value of the
* motorway junction node</li>
* <li>mkgmap:exit_hint_name: Tagged with the name tag value of the motorway
* junction node</li>
* </ul>
* Style implementors can use the common Garmin code 0x09 for motorway_links
* and any other routable id (except 0x08 and 0x09) for the links with
* mkgmap:exit_hint=true and/or mkgmap:dest_hint=*. The naming of this
* middle way can be typically assigned from destination, ref, destination:ref,
* mkgmap:exit_hint_ref, mkgmap:exit_hint_name and/or mkgmap:exit_hint_exit_to.
*/
private void processWays() {
// remove the adjacent links from the destinationLinkWays list
// to avoid duplicate dest_hints
Queue<Way> linksWithDestination = new ArrayDeque<Way>();
linksWithDestination.addAll(destinationLinkWays.values());
log.debug(destinationLinkWays.size(),"links with destination tag");
while (linksWithDestination.isEmpty()== false) {
Way linkWay = linksWithDestination.poll();
String destination = linkWay.getTag("mkgmap:dest_hint_work");
if (log.isDebugEnabled())
log.debug("Check way",linkWay.getId(),linkWay.toTagString());
// Retrieve all adjacent ways of the current link
Coord c = linkWay.getPoints().get(linkWay.getPoints().size()-1);
if (isOnewayOppositeDirection(linkWay)) {
c = linkWay.getPoints().get(0);
}
Set<Way> nextWays = adjacentWays.get(c);
if (nextWays != null) {
for (Way connectedWay : nextWays) {
String nextDest = connectedWay.getTag("mkgmap:dest_hint_work");
if (log.isDebugEnabled())
log.debug("Followed by",connectedWay.getId(),connectedWay.toTagString());
// remove the way from destination handling only if both ways are connected with start/end points
// otherwise it is a crossroads and therefore both ways need to be handled
boolean startEndConnection = connectedWay.getPoints().isEmpty()==false && connectedWay.getPoints().get(0).equals(c);
if (startEndConnection && connectedWay.equals(linkWay) == false
&& connectedWay.getTag("highway").endsWith("_link")
&& destination.equals(nextDest)) {
// do not use this way because there is another link before that with the same destination
destinationLinkWays.remove(connectedWay.getId());
if (log.isDebugEnabled())
log.debug("Removed",connectedWay.getId(),connectedWay.toTagString());
}
}
}
}
log.debug(destinationLinkWays.size(),"links with destination tag after cleanup");
if (processExits) {
// collect all nodes of highway=motorway/trunk ways so that we can check if an exit node
// belongs to a motorway/trunk or is a "subexit" within a motorway/trunk junction
Map<String, Set<Coord>> highwayCoords = new LinkedHashMap<>();
for (String type : highwayTypes){
highwayCoords.put(type, new HashSet<Coord>());
}
for (Way w : saver.getWays().values()) {
String highwayTag = w.getTag("highway");
if (highwayTag == null)
continue;
if (highwayTypes.contains(highwayTag)){
Set<Coord> set = highwayCoords.get(highwayTag);
set.addAll(w.getPoints());
}
}
// get all nodes tagged with highway=motorway_junction
for (Node exitNode : saver.getNodes().values()) {
if (isTaggedAsExit(exitNode) && saver.getBoundingBox().contains(exitNode.getLocation())) {
String expectedHighwayTag = null;
for (Entry<String, Set<Coord>> entry : highwayCoords.entrySet()){
if (entry.getValue().contains(exitNode.getLocation())){
expectedHighwayTag = entry.getKey();
break;
}
}
if (expectedHighwayTag == null){
// use exits only if they are located on a motorway or trunk
if (log.isDebugEnabled())
log.debug("Skip non highway exit:", exitNode.toBrowseURL(), exitNode.toTagString());
continue;
}
// retrieve all ways with this exit node
Set<Way> exitWays = adjacentWays.get(exitNode.getLocation());
if (exitWays==null) {
log.debug("Exit node", exitNode, "has no connected ways. Skip it.");
continue;
}
// retrieve the next node on the highway to be able to check if
// the inserted node has the correct orientation
List<Entry<Coord, Way>> nextNodes = getNextNodes(exitNode.getLocation(), true);
Coord nextHighwayNode = null;
int countMatches = 0;
for (Entry<Coord, Way> nextNode : nextNodes) {
if (expectedHighwayTag.equals(nextNode.getValue().getTag("highway"))) {
nextHighwayNode = nextNode.getKey();
countMatches++;
}
}
if (countMatches > 1){
// may happen when the highway is a link which splits further into two or more links
// ignore the node
nextHighwayNode = null;
}
// use link ways only
for (Way w : exitWays) {
destinationLinkWays.remove(w.getId());
if (isNotOneway(w)) {
log.warn("Ignore way",w,"because it is not oneway");
continue;
}
if (w.isViaWay()){
log.warn("Ignore way",w,"because it is a via way in a restriction relation");
continue;
}
String highwayLinkTag = w.getTag("highway");
if (highwayLinkTag.endsWith("_link")) {
log.debug("Try to cut",highwayLinkTag, w, "into three parts for giving hint to exit", exitNode);
// calc the way length to decide how to cut the way
double wayLength = getLength(w);
if (wayLength < 10 && w.getPoints().size() < 3) {
log.info("Way", w, "is too short (", wayLength," m) to cut it into several pieces. Cannot place exit hint.");
continue;
}
// now create three parts:
// wayPart1: original tags only
// hintWay: original tags plus the mkgmap:exit_hint* tags
// w: rest of the original way
double cut1 = Math.min(wayLength/2,20.0);
double cut2 = Math.min(wayLength, 100);
Way wayPart1 = cutoffWay(w,cut1, cut2, exitNode.getLocation(), nextHighwayNode);
if (wayPart1 == null) {
log.info("Way", w, "is too short to cut at least ",cut1,"m from it. Cannot create exit hint.");
continue;
} else {
if (log.isDebugEnabled())
log.debug("Cut off way", wayPart1, wayPart1.toTagString());
}
Way hintWay = w;
if (wayLength > 50) {
hintWay = cutoffWay(w, 10.0, 50.0, exitNode.getLocation(), nextHighwayNode);
}
if (hintWay == null) {
log.info("Way", w, "is too short to cut at least 20m from it. Cannot create exit hint.");
} else {
hintWay.addTag("mkgmap:exit_hint", "true");
if (processDestinations) {
String hint = hintWay.getTag("mkgmap:dest_hint_work");
if (hint != null){
hintWay.deleteTag("mkgmap:dest_hint_work");
hintWay.addTag("mkgmap:dest_hint", hint);
}
}
if (exitNode.getTag("ref") != null)
hintWay.addTag("mkgmap:exit_hint_ref", exitNode.getTag("ref"));
if (countMatches == 1){
if (exitNode.getTag("exit_to") != null){
hintWay.addTag("mkgmap:exit_hint_exit_to", exitNode.getTag("exit_to"));
}
}
if (getName(exitNode) != null){
hintWay.addTag("mkgmap:exit_hint_name", getName(exitNode));
}
if (log.isInfoEnabled())
log.info("Cut off exit hint way", hintWay, hintWay.toTagString());
}
}
}
}
}
}
if (processDestinations) {
// use link ways only
while (destinationLinkWays.isEmpty() == false) {
Way w = destinationLinkWays.values().iterator().next();
destinationLinkWays.remove(w.getId());
if (isNotOneway(w)) {
log.warn("Ignore way",w,"because it is not oneway");
continue;
}
if (w.isViaWay()){
log.warn("Ignore way",w,"because it is a via way in a restriction relation");
continue;
}
String highwayLinkTag = w.getTag("highway");
if (highwayLinkTag.endsWith("_link")) {
log.debug("Try to cut",highwayLinkTag, w, "into three parts for giving hint");
Coord firstNode = w.getPoints().get(0);
Coord secondNode = w.getPoints().get(1);
// retrieve the next node on the highway to be able to check if
// the inserted node has the correct orientation
List<Entry<Coord, Way>> nextNodes = getNextNodes(firstNode, true);
Coord nextHighwayNode = null;
double angle = Double.MAX_VALUE;
for (Entry<Coord, Way> nextNode : nextNodes) {
if (nextNode.getValue().equals(w)) {
continue;
}
double thisAngle = getAngle(firstNode, secondNode, nextNode.getKey());
if (Math.abs(thisAngle) < angle) {
angle = Math.abs(thisAngle);
nextHighwayNode = nextNode.getKey();
}
}
// calc the way length to decide how to cut the way
double wayLength = getLength(w);
if (wayLength < 10) {
log.info("Way", w, "is too short (", wayLength," m) to cut it into several pieces. Cannot place destination hint.");
continue;
}
// now create three parts:
// wayPart1: original tags only
// hintWay: original tags plus the mkgmap:exit_hint* tags
// w: rest of the original way
double cut1 = Math.min(wayLength/2, 20.0);
double cut2 = Math.min(wayLength, 100);
Way wayPart1 = cutoffWay(w, cut1, cut2, firstNode, nextHighwayNode);
if (wayPart1 == null) {
log.info("Way", w, "is too short to cut at least 10m from it. Cannot create destination hint.");
continue;
} else {
if (log.isDebugEnabled())
log.debug("Cut off way", wayPart1, wayPart1.toTagString());
}
Way hintWay = w;
if (wayLength > 50) {
hintWay = cutoffWay(w, 10.0, 50.0, firstNode, nextHighwayNode);
}
if (hintWay == null) {
log.info("Way", w, "is too short to cut at least 20m from it. Cannot create destination hint.");
} else {
String hint = hintWay.getTag("mkgmap:dest_hint_work");
if (hint != null){
hintWay.deleteTag("mkgmap:dest_hint_work");
hintWay.addTag("mkgmap:dest_hint", hint);
} else {
log.error("Internal error in process_destination with way",hintWay);
}
if (log.isInfoEnabled())
log.info("Cut off exit hint way", hintWay, hintWay.toTagString());
}
}
}
}
}
/**
* Retrieves the angle in clockwise direction between the line (cCenter, c1)
* and the line (cCenter, c2).
* @param cCenter the common point of both lines
* @param c1 point of the first line
* @param c2 point of the second line
* @return the angle [-180; 180]
*/
private double getAngle(Coord cCenter, Coord c1, Coord c2)
{
double dx1 = c1.getLongitude() - cCenter.getLongitude();
double dy1 = -(c1.getLatitude() - cCenter.getLatitude());
double dx2 = c2.getLongitude() - cCenter.getLongitude();
double dy2 = -(c2.getLatitude() - cCenter.getLatitude());
double inRads1 = Math.atan2(dy1,dx1);
double inRads2 = Math.atan2(dy2,dx2);
return Math.toDegrees(inRads2) - Math.toDegrees(inRads1);
}
/**
* Cleans all internal data that is no longer used after the hook has been processed.
*/
private void cleanup() {
adjacentWays = null;
wayNodes = null;
destinationLinkWays = null;
linkTypes = null;
saver = null;
nameTags = null;
}
public Set<String> getUsedTags() {
if (!(processDestinations || processExits))
return Collections.emptySet();
// When processing destinations also load the destination:lanes,forward and backward tag
// to be able to copy the value to the destination tag
// Do not load destination because it makes sense only if the tag is
// referenced in the style file
Set<String> tags = new HashSet<String>();
tags.add("highway");
tags.add("destination");
tags.add("destination:lanes");
tags.add("destination:lanes:forward");
tags.add("destination:lanes:backward");
tags.add("destination:forward");
tags.add("destination:backward");
tags.add("destination:street");
if (processExits){
tags.add("exit_to");
tags.add("ref");
}
return tags;
}
public void end() {
log.info("LinkDestinationHook started");
retrieveWays();
if (processExits || processDestinations)
processWays();
cleanup();
log.info("LinkDestinationHook finished");
}
/**
* Retrieves if the given way is tagged as oneway in the direction of the way.
* @param w the way
* @return <code>true</code> way is oneway
*/
private boolean isOnewayInDirection(Way w) {
if (w.tagIsLikeYes("oneway")) {
return true;
}
// check if oneway is set implicitly by the highway type (motorway and motorway_link)
String onewayTag = w.getTag("oneway");
String highwayTag = w.getTag("highway");
if (onewayTag == null && highwayTag != null
&& (highwayTag.equals("motorway") || highwayTag.equals("motorway_link"))) {
return true;
}
return false;
}
/**
* Retrieves if the given way is tagged as oneway but in opposite direction of the way.
* @param w the way
* @return <code>true</code> way is oneway in opposite direction
*/
private boolean isOnewayOppositeDirection(Way w) {
return "-1".equals(w.getTag("oneway"));
}
/**
* Retrieves if the given way is not oneway.
* @param w the way
* @return <code>true</code> way is not oneway
*/
private boolean isNotOneway(Way w) {
return "no".equals(w.getTag("oneway")) ||
(isOnewayInDirection(w) == false
&& isOnewayOppositeDirection(w) == false);
}
/** Private length function without caching */
private LengthFunction length = new LengthFunction() {
public boolean isCached() {
return false;
}
};
/**
* Retrieve the length of the given way.
* @param w way
* @return length in m
*/
private double getLength(Way w) {
String lengthValue = length.value(w);
try {
return Math.round(Double.valueOf(lengthValue));
} catch (Exception exp) {
return 0;
}
}
}