Subversion Repositories mkgmap

Rev

Rev 4551 | 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.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.List;
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.osmstyle.NameFinder;
import uk.me.parabola.mkgmap.osmstyle.housenumber.ExtNumbers;
import uk.me.parabola.util.EnhancedProperties;
import uk.me.parabola.util.MultiHashMap;

/**
 * Cuts link ways into parts and adds destination/exit hints to a new part so
 * that the Garmin is able to display a valid destination.
 * @author Gerd Petermann
 * @author WanMil (initial versions)
 */

public class LinkDestinationHook implements OsmReadingHooks {
        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<>();
        /** Contains all _link ways that have to be processed */
        private LinkedHashSet<Way> destinationLinkWays = new LinkedHashSet<>();
       
        private static final Set<String> highwayTypes = new LinkedHashSet<>(Arrays.asList(
                        "motorway", "trunk", "primary", "secondary", "tertiary",
                        "motorway_link", "trunk_link", "primary_link", "secondary_link", "tertiary_link"));
        private static final Set<String> linkTypes = new LinkedHashSet<>(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 NameFinder nameFinder;

        /** Maps which nodes contains to which ways */
        private IdentityHashMap<Coord, Set<Way>> wayNodes = new IdentityHashMap<>();
       
        private boolean processDestinations;
        private boolean processExits;
       
        private static final short TK_HIGHWAY = TagDict.getInstance().xlate("highway");
        private static final short TK_ONEWAY = TagDict.getInstance().xlate("oneway");
        private static final short TK_EXIT_TO = TagDict.getInstance().xlate("exit_to");
        private static final short TKM_DEST_HINT_WORK = TagDict.getInstance().xlate("mkgmap:dest_hint_work");
       
        @Override
        public boolean init(ElementSaver saver, EnhancedProperties props, Style style) {
                this.saver = saver;
                nameFinder = new NameFinder(props);
                processDestinations = props.containsKey("process-destination");
                processExits = props.containsKey("process-exits");
                return processDestinations || processExits;
        }

        /**
         * Fills the internal lists
         */

        private void retrieveWaysAndRelations() {
                // 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(TK_HIGHWAY);
                        if (highwayTag != null && highwayTypes.contains(highwayTag)) {
                                processHighWay(w, highwayTag);
                        }
                }
               
                // 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);
                        }
                }
        }
       
        private void processHighWay(Way w, String highwayTag) {
                // 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;
                String directionSuffix = null;
               
                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);
                        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());
                        directionSuffix = "backward";
                } else {
                        points = w.getPoints();
                }
                for (Coord c : points) {
                        adjacentWays.computeIfAbsent(c, k -> new HashSet<>(4)).add(w);
                }
                registerPointsOfWay(w);
                checkIfUsableLink(w, highwayTag, directionSuffix);
        }

        private void checkIfUsableLink(Way w, String highwayTag, String directionSuffix) {
                // 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))
                        return;
               
                final String destinationLanes = "destination:lanes";
                final String standardTagKey = "destination";
                String destSourceTagKey = standardTagKey; // for log messages
                String destHint = w.getTag(standardTagKey);
                if (destHint == null) {
                        // destination is not set
                        // => check if destination:lanes is without any lane specific information (no |)
                        destSourceTagKey = destinationLanes;
                        String destLanesTag = w.getTag(destSourceTagKey);
                        if (destLanesTag == null && directionSuffix != null) {
                                destSourceTagKey += ":" + directionSuffix;
                                destLanesTag = w.getTag(destSourceTagKey);
                        }
                        if (destLanesTag != null && !destLanesTag.contains("|")) {
                                // the destination:lanes tag contains no | => no lane specific information
                                // use this tag as destination tag
                                destHint = destLanesTag;
                        }
                       
                        if (destHint == null && directionSuffix != null) {
                                // use the destination:forward or :backward value
                                destSourceTagKey = "destination:" + directionSuffix;
                                destHint = w.getTag(destSourceTagKey);
 
                        }
                        if (destHint == null) {
                                // try to use the destination:street value
                                destSourceTagKey = "destination:street";
                                destHint = w.getTag(destSourceTagKey);
                        }
                }

                if (destHint != null) {
                        w.addTag(TKM_DEST_HINT_WORK, destHint);
                        destinationLinkWays.add(w);

                        if (log.isDebugEnabled() && !standardTagKey.equals(destSourceTagKey)) {
                                String msg = destSourceTagKey.startsWith(destinationLanes)
                                                ? "as destination tag because there is one lane information only." : "as destination tag.";
                                log.debug("Use", destSourceTagKey, msg, "Way", w.getId(), w.toTagString());
                        }
                }
        }

        /**
         * 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()) {
                        wayNodes.computeIfAbsent(c, k-> new HashSet<>()).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();

        }
       
        /**
         * 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()) {
                        log.error("internal error: via way is split in", this.getClass().getSimpleName());
                }
                // create a copy because original list may be modified within the loop
                for (RestrictionRelation rr : new ArrayList<>(wayRestrictions)) {
                        Coord lastPointNewWay = newWay.getFirstPoint();
                        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) {
                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.markAsGeneratedFrom(w);
                                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.getFirstPoint();
                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);
                                // check if we find a point which is closer to Garmin point in 24 resolution
                                Coord alternative = ExtNumbers.rasterLineNearPoint(c, lastC, cConnection, false);
                                if (alternative != null)
                                        cConnection = alternative;
                                // create the new way with identical tags
                                w.getPoints().add(i, cConnection);
                                Way precedingWay = new Way(w.getOriginalId(), new ArrayList<>(w.getPoints().subList(0, i + 1)));
                                precedingWay.markAsGeneratedFrom(w);
                                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;
        }
       
        /**
         * Retrieves if the given node is tagged as usable 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> if the node is a usable exit, else <code>false</code>
         */

        private boolean isTaggedAsExit(Node node) {
                return "motorway_junction".equals(node.getTag(TK_HIGHWAY))
                                && (node.getTag("ref") != null || (nameFinder.getName(node) != null) || node.getTag(TK_EXIT_TO) != null);
        }
       
        /**
         * Cuts major roads 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() {
                cleanupLinkDestWays();
                createExitHints();
                createDestinationHints();
        }

        private void cleanupLinkDestWays() {
                // remove the adjacent links from the destinationLinkWays list
                // to avoid duplicate dest_hints
                Queue<Way> linksWithDestination = new ArrayDeque<>(destinationLinkWays);
                log.debug(destinationLinkWays.size(),"links with destination tag");
                while (!linksWithDestination.isEmpty()) {
                        Way linkWay = linksWithDestination.poll();
                        String destination = linkWay.getTag(TKM_DEST_HINT_WORK);
                        if (log.isDebugEnabled())
                                log.debug("Check way", linkWay.getId(), linkWay.toTagString());
                       
                        // Retrieve all adjacent ways of the current link
                        Coord c = isOnewayOppositeDirection(linkWay) ? linkWay.getFirstPoint() : linkWay.getLastPoint();
                       
                        Set<Way> nextWays = adjacentWays.get(c);
                        if (nextWays != null) {
                                for (Way connectedWay : nextWays) {
                                        String nextDest = connectedWay.getTag(TKM_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
                                        Coord c2 = isOnewayOppositeDirection(connectedWay) ? connectedWay.getLastPoint()
                                                        : connectedWay.getFirstPoint();

                                        boolean startEndConnection = c == c2;
                                        if (startEndConnection && !connectedWay.equals(linkWay)
                                                        && connectedWay.getTag(TK_HIGHWAY).endsWith("_link")
                                                        && destination.equals(nextDest)) {
                                                // do not use this way because there is another link before that with the same destination
                                                boolean removed = destinationLinkWays.remove(connectedWay);
                                                if (removed && log.isDebugEnabled()) {
                                                        log.debug("Removed", connectedWay.getId(), connectedWay.toTagString());
                                                }
                                        }
                                }
                        }
                }
                log.debug(destinationLinkWays.size(),"links with destination tag after cleanup");
        }

        private void createExitHints() {
                if(!processExits)
                        return;
               
                List<String> hwSorted = new ArrayList<>(highwayTypes);
                for (Node exitNode : saver.getNodes().values()) {
                        if (isTaggedAsExit(exitNode) && saver.getBoundingBox().contains(exitNode.getLocation())) {
                                // node is tagged with highway=motorway_junction and has usable exit info
                                processExitNode(exitNode, hwSorted);
                        }
                }
        }

        /**
         * Check if we can add an exit hint for the given exit node.
         * @param exitNode
         * @param hwSorted
         * @param highwayCoords
         */

        private void processExitNode(Node exitNode, List<String> hwSorted) {
                // 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.");
                        return;
                }
                String exitTo = exitNode.getTag(TK_EXIT_TO);
                if (exitTo != null) {
                        int countMatches = 0;
                        int preferred = Integer.MAX_VALUE;
                        for (Way w : exitWays) {
                                String hw = w.getTag(TK_HIGHWAY);
                                int pos = hwSorted.indexOf(hw);
                                if (pos < preferred) {
                                        preferred = pos;
                                        countMatches = 1;
                                } else if (pos == preferred) {
                                        countMatches++;
                                }
                        }
                        if (countMatches != 1)
                                exitTo = null; // don't use exit_to info, it typically contains info for all the ways
                }

                // use link ways only
                for (Way w : exitWays) {
                        destinationLinkWays.remove(w);
                        if(!canSplit(w))
                                continue;
                        processExitWay(exitNode, w, exitTo);
                }
        }

        private void processExitWay(Node exitNode, Way w, String exitTo) {
                String highwayLinkTag = w.getTag(TK_HIGHWAY);
                if (highwayLinkTag.endsWith("_link")) {
                        log.debug("Try to cut", highwayLinkTag, w, "into three parts for giving hint to exit", exitNode);
                        Way hintWay = splitWay(w, "exit");
                        if (hintWay != null) {
                                fixDestHint(hintWay);
                                hintWay.addTag("mkgmap:exit_hint", "true");
                                if (exitNode.getTag("ref") != null)
                                        hintWay.addTag("mkgmap:exit_hint_ref", exitNode.getTag("ref"));
                                if (exitTo != null) {
                                        hintWay.addTag("mkgmap:exit_hint_exit_to", exitTo);
                                }
                                if (nameFinder.getName(exitNode) != null) {
                                        hintWay.addTag("mkgmap:exit_hint_name", nameFinder.getName(exitNode));
                                }

                                if (log.isInfoEnabled())
                                        log.info("Cut off exit hint way", hintWay, hintWay.toTagString());
                        }
                }
        }

        private String fixDestHint(Way hintWay) {
                if (processDestinations) {
                        String hint = hintWay.getTag(TKM_DEST_HINT_WORK);
                        if (hint != null) {
                                hintWay.deleteTag(TKM_DEST_HINT_WORK);
                                hintWay.addTag("mkgmap:dest_hint", hint);
                        }
                        return hint;
                }
                return null;
        }

        private void createDestinationHints() {
                if (!processDestinations)
                        return;
                // use link ways only
                while (!destinationLinkWays.isEmpty()) {
                        Way w = destinationLinkWays.iterator().next();
                        destinationLinkWays.remove(w);
                        String highwayLinkTag = w.getTag(TK_HIGHWAY);
                        if (!canSplit(w) || !highwayLinkTag.endsWith("_link"))
                                continue;

                        log.debug("Try to cut",highwayLinkTag, w, "into three parts for giving hint");

                        Way hintWay = splitWay(w, "destination");
                        if (hintWay != null) {
                                String hint = fixDestHint(hintWay);
                                if (hint == null) {
                                        log.error("Internal error in process_destination with way", hintWay);
                                }

                                if (log.isInfoEnabled())
                                        log.info("Cut off exit hint way", hintWay, hintWay.toTagString());
                        }
                }
        }

        /**
         * Try to split the way. If successful, it creates two or three parts.
         * <ul>
         * <li>wayPart1: original tags only</li>
         * <li>hintWay: original tags plus the mkgmap:exit_hint* tags</li>
         * <li>rest of the original way (if any remains)</li>
         * </ul>
         * @param w the way to split
         * @param hintType the type of hint that is added (exit or destination), use for logging only
         * @return null if not successful, else the hintWay
         */

        private Way splitWay(Way w, String  hintType) {
                // calc the way length to decide how to cut the way
                double wayLength = w.calcLengthInMetres();
                double cut1 = Math.min(wayLength / 2, 20.0);
                double cut2 = Math.min(wayLength, 100);
                Way wayPart1 = cutoffWay(w, cut1, cut2);
                if (wayPart1 == null) {
                        log.info("Way", w, "is too short to cut at least", cut1, "m from it. Cannot create", hintType, "hint.");
                        return null;
                }
                if (log.isDebugEnabled())
                        log.debug("Cut off way", wayPart1, wayPart1.toTagString());
               
                Way hintWay = w;
                if (wayLength > 50) {
                        hintWay = cutoffWay(w, 10.0, 50.0);
                }
                if (hintWay == null) {
                        log.info("Way", w, "is too short to cut at least 20m from it. Cannot create", hintType, "hint.");
                }
                return hintWay;
        }

        private static boolean canSplit(Way w) {
                if (isNotOneway(w)) {
                        log.warn("Ignore way", w, "because it is not oneway");
                        return false;
                }
                if (w.isViaWay()) {
                        log.warn("Ignore way", w, "because it is a via way in a restriction  relation");
                        return false;
                }
                return w.calcLengthInMetres() >= 0.3;
        }

        /**
         * Cleans all internal data that is no longer used after the hook has been processed.
         */

        private void cleanup() {
                adjacentWays = null;
                wayNodes = null;
                destinationLinkWays = null;
                saver = null;
                nameFinder = null;
        }

        @Override
        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<>();
                tags.add("highway");
                tags.add("oneway");
                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;
        }      

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

                retrieveWaysAndRelations();
                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 static boolean isOnewayInDirection(Way w) {
                if (w.tagIsLikeYes(TK_ONEWAY)) {
                        return true;
                }
               
                // check if oneway is set implicitly by the highway type (motorway and motorway_link)
                String onewayTag = w.getTag(TK_ONEWAY);
                String highwayTag = w.getTag(TK_HIGHWAY);
                return onewayTag == null && highwayTag != null
                                && ("motorway".equals(highwayTag)|| "motorway_link".equals(highwayTag));
        }

        /**
         * 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 static boolean isOnewayOppositeDirection(Way w) {
                return "-1".equals(w.getTag(TK_ONEWAY));
        }

        /**
         * Retrieves if the given way is not oneway.
         * @param w the way
         * @return <code>true</code> way is not oneway
         */

        private static boolean isNotOneway(Way w) {
                return "no".equals(w.getTag(TK_ONEWAY)) || (!isOnewayInDirection(w) && !isOnewayOppositeDirection(w));
        }
}