Subversion Repositories mkgmap

Rev

Rev 2461 | View as "text/plain" | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * 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.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
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.util.EnhancedProperties;

/**
 * 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 Queue<Way> destinationLinkWays = new ArrayDeque<Way>();

        private HashSet<String> tagValues = new HashSet<String>(Arrays.asList(
                        "motorway_link", "trunk_link"));

        private List<String> nameTags;

        /** Maps which nodes contains to which ways */
        private IdentityHashMap<Coord, Set<Way>> wayNodes = new IdentityHashMap<Coord, Set<Way>>();
        private boolean onlyMotorwayExitHint;
       
        private boolean processDestinations;
        private boolean processExits;
       
        public boolean init(ElementSaver saver, EnhancedProperties props) {
                this.saver = saver;
                nameTags = LocatorUtil.getNameTags(props);
                onlyMotorwayExitHint = props.containsKey("all-exit-hints") == false;
                processDestinations = props.containsKey("process-destination");
                processExits = props.containsKey("process-exits");
                return processDestinations || processExits;
        }

        /**
         * Fills the internal lists with the
         */

        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) {
                                // the points of the way are kept so that it is easy to get
                                // the adjacent ways for a given _link way
                                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);
                                } 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());
                                } 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);
                                }
                                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);
                                }

                                // if the way is a link way and has a destination tag
                                // put it the list of ways that have to be processed
                                if (tagValues.contains(highwayTag)
                                                && w.getTag("destination") != null) {
                                        destinationLinkWays.add(w);
                                }
                        }
                }
        }
       
        /**
         * Copies the destination tags of all motorway_link and trunk_link ways to the adjacent ways.
         */

        private void processDestinations() {
                // process all links with a destination tag
                // while checking new ways can be added to the list
                while (destinationLinkWays.isEmpty() == false) {
                        Way link = destinationLinkWays.poll();

                        if (isNotOneway(link)) {
                                // non oneway links cannot be handled. The destination tag is probably wrong.
                                if (log.isInfoEnabled())
                                        log.info("Link is not oneway. Do not handle it.", link);
                                continue;
                        }

                        // get the last point of the link to retrieve all connected ways
                        Coord connectPoint = link.getPoints().get(link.getPoints().size() - 1);
                        if (isOnewayOppositeDirection(link)) {
                                // for reverse oneway ways it's the first point
                                connectPoint = link.getPoints().get(0);
                        }

                        String destinationTag = link.getTag("destination");
                       
                        Set<Way> connectedWays = adjacentWays.get(connectPoint);
                        if (connectedWays == null) {
                                connectedWays = Collections.emptySet();
                        } else {
                                connectedWays = new HashSet<Way>(connectedWays);
                        }
                        connectedWays.remove(link);

                        if (log.isInfoEnabled())
                                log.info("Link", link);
                        for (Way connection : connectedWays) {
                                if (connection.getTag("destination") != null) {
                                        if (log.isInfoEnabled())
                                                log.info("Way already has destionation tag", connection);
                                        continue;
                                }
                                if (log.isInfoEnabled())
                                        log.info("-", connection);

                                // check if the connected way can have an unambiguous direction
                                // => it must not have more than one further connections
                                boolean oneDestination = true;
                                List<Coord> pointsToCheck;
                                if (isOnewayInDirection(connection)) {
                                        pointsToCheck = connection.getPoints();
                                } else if (isOnewayOppositeDirection(connection)) {
                                        pointsToCheck = new ArrayList<Coord>(
                                                        connection.getPoints());
                                        Collections.reverse(pointsToCheck);
                                } else {
                                        pointsToCheck = Collections.emptyList();
                                        if (log.isInfoEnabled())
                                                log.info("Way is not oneway - do not add destination", connection);
                                        oneDestination = false;
                                }
                                // check if the target way has an unambiguous destination
                                // so it has no other connections to other destinations
                                for (Coord c : pointsToCheck) {
                                        if (c.equals(connectPoint)) {
                                                break;
                                        }
                                        Set<Way> furtherConnects = adjacentWays.get(c);
                                        for (Way fc : furtherConnects) {
                                                String fcDest = fc.getTag("destination");
                                                if (fcDest != null
                                                                && destinationTag.equals(fcDest) == false) {
                                                        if (log.isInfoEnabled())
                                                                log.info("Ambiguous destination:", destinationTag, "!=", fcDest);
                                                        oneDestination = false;
                                                        break;
                                                }
                                        }
                                        if (oneDestination == false) {
                                                break;
                                        }
                                }

                                if (oneDestination) {
                                        if (log.isInfoEnabled())
                                                log.info("Add destination=" + destinationTag, "to", connection);
                                        connection.addTag("destination", destinationTag);

                                        if (tagValues.contains(connection.getTag("highway"))) {
                                                // it is a link so process that way too
                                                destinationLinkWays.add(connection);
                                        }
                                }
                        }
                }
        }
       
        /**
         * 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;
        }
       
        /**
         * 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 minLength the cut off way must have at least this length
         * @return the cut off way or <code>null</code> if cutting not possible
         */

        private Way cutoffWay(Way w, double minLength, Coord c1, Coord c2) {
                if (w.getPoints().size()<2) {
                        return null;
                }
               
                boolean useExistingPoints = w.getPoints().size() >= 3;
                useExistingPoints = false;
                double remainLength = minLength;
                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);
                       
                        // do not use the last point of the way
                        // instead it need to be split in such a case
                        useExistingPoints = useExistingPoints && i < w.getPoints().size()-1;
                       
                        if (useExistingPoints && remainLength - segmentLength <= 0.0) {
                                // create a new way with all points 0..i and identical tags
                                Way precedingWay = new Way(FakeIdGenerator.makeFakeId(), new ArrayList<Coord>(w.getPoints().subList(0, i+1)));
                                precedingWay.copyTags(w);
                                // the first node of the new way is now used by the org and the new way
                                precedingWay.getPoints().get(0).incHighwayCount();
                               
                                saver.addWay(precedingWay);
                                // remove the points of the new way from the original way
                                w.getPoints().subList(0, i).clear();
                               
                                // return the new way
                                return precedingWay;
                        }
                       
                        if (useExistingPoints == false && remainLength - segmentLength <=  0.0d) {
                                double frac = remainLength / segmentLength;
                                // insert a new point and 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;
                                        }
                                }
                               
                                // the new point is used by the old and the new way
                                cConnection.incHighwayCount();
                                cConnection.incHighwayCount();
                               
                                w.getPoints().add(i,cConnection);
                               
                                // create the new way with identical tags
                                Way precedingWay = new Way(FakeIdGenerator.makeFakeId(), new ArrayList<Coord>(w.getPoints().subList(0, i+1)));
                                precedingWay.copyTags(w);
                               
                                saver.addWay(precedingWay);
                               
                                // remove the points of the new way from the old way
                                w.getPoints().subList(0, i).clear();
                               
                                // return the split way
                                return precedingWay;                   
                        }              
                        remainLength -= segmentLength;
                        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));
                        }
                }
                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 ways connected to an exit node
         * (highway=motorway_junction) into three parts to be able to get a hint on
         * Garmin GPS. The mid part way is tagged additionally with the following
         * tags:
         * <ul>
         * <li>mkgmap:exit_hint=true</li>
         * <li>mkgmap:exit_hint_ref: Tagged with the ref tag value of the exit node</li>
         * <li>mkgmap:exit_hint_exit_to: Tagged with the exit_to tag value of the exit node</li>
         * <li>mkgmap:exit_hint_name: Tagged with the name tag value of the exit
         * node</li>
         * </ul>
         * Style implementors can use the common Garmin code 0x09 for motorway_links
         * and the motorway code 0x01 for the links with mkgmap:exit_hint=true. The
         * naming of this middle way can be assigned from mkgmap:exit_hint_ref and
         * mkgmap:exit_hint_name.
         */

        private void createExitHints() {
                // collect all nodes of highway=motorway ways so that we can check if an exit node
                // belongs to a motorway or is a "subexit" within a motorway junction
                Set<Coord> highwayCoords = new HashSet<Coord>();
                for (Way w : saver.getWays().values()) {
                        if ("motorway".equals(w.getTag("highway"))) {
                                highwayCoords.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())) {
                               
                                boolean isHighwayExit = highwayCoords.contains(exitNode.getLocation());
                                // use exits only if they are located on a motorway
                                if (onlyMotorwayExitHint && isHighwayExit == false) {
                                        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;
                                }
                               
                                // if the exit is on a motorway_link it must be ensured that the hint is
                                // created on the right / left link only
                                if (onlyMotorwayExitHint == false && isHighwayExit == false) {
//                                      List<Entry<Coord, Way>> prevNodes = getNextNodes(exitNode.getLocation(), false);
//                                      log.error("Prev nodes: "+prevNodes);
//                                      if (prevNodes.isEmpty()) {
//                                              log.error("Node "+exitNode+" has no predecessor way. Cannot create exit hint for it.");
//                                              continue;
//                                      }
//                                      if (prevNodes.size() > 1) {
//                                              log.error("Node "+exitNode+" has multiple predecessor ways. Cannot create exit hint for it.");
//                                              continue;
//                                      }
//                                     
//                                      // there is one predecessor way
//                                      Coord predecessorNode = prevNodes.get(0).getKey();
                                       
//                                      List<Entry<Coord, Way>> nextNodes = getNextNodes(exitNode.getLocation(), true);
//                                      log.error("Next nodes: "+nextNodes);
//                                     
//                                      // TODO: differ drive-on-left and drive-on-right
//                                      // at the moment select the rightmost way
//                                      Entry<Coord, Way> possibleExit = null;
//                                      for (Entry<Coord, Way> checkWay : nextNodes) {
//                                              if (possibleExit == null) {
//                                                      possibleExit = checkWay;
//                                              } else {
//                                                     
//                                              }
//                                      }
                                       
                                }

                                // 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;
                                for (Entry<Coord, Way> nextNode : nextNodes) {
                                        if ("motorway".equals(nextNode.getValue().getTag("highway"))) {
                                                nextHighwayNode = nextNode.getKey();
                                                break;
                                        }
                                }
                               
                                // use the motorway_link ways only
                                for (Way w : exitWays) {
                                        if ("motorway_link".equals(w.getTag("highway"))) {
                                                log.debug("Try to cut motorway_link", w, "into three parts for giving hint to exit", exitNode);

                                                // now create three parts:
                                                // wayPart1: 10m having the original tags only
                                                // hintWay: 10m having the original tags plus the mkgmap:exit_hint* tags
                                                // w: the rest of the original way
                                               
                                                Way wayPart1 = cutoffWay(w,10.0, exitNode.getLocation(), nextHighwayNode);
                                                if (wayPart1 == null) {
                                                        log.info("Way", w, "is too short to cut at least 10m from it. Cannot create exit hint.");
                                                        continue;
                                                } else {
                                                        if (log.isDebugEnabled())
                                                                log.debug("Cut off way", wayPart1, wayPart1.toTagString());
                                                }
                                               
                                                Way hintWay = cutoffWay(w,10.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 (exitNode.getTag("ref") != null)
                                                                hintWay.addTag("mkgmap:exit_hint_ref", exitNode.getTag("ref"));
                                                        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());
                                                }

                                        }
                                }
                        }
                }
        }
       
        /**
         * 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;
                tagValues = null;
                saver = null;
                nameTags = null;
        }

// Do not return any used tag because this hook only has an effect if the tag destination is
// additionally used in the style file.
//      public Set<String> getUsedTags() {
//              // TODO Auto-generated method stub
//              return super.getUsedTags();
//      }      

        public void end() {
                log.info("LinkDestinationHook started");

                retrieveWays();
               
                if (processDestinations)
                        processDestinations();
                if (processExits)
                        createExitHints();
               
                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.isBoolTag("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);
        }

}