Subversion Repositories mkgmap

Rev

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

/*
 * Copyright (C) 2011.
 *
 * 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.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;

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.function.AreaSizeFunction;
import uk.me.parabola.util.EnhancedProperties;

/**
 * Adds a POI for each area and multipolygon with the same tags in case the add-pois-to-areas option
 * is set. Adds multiple POIs to each line if the add-pois-to-lines option is set.<br/>
 * <br/>
 * <code>add-pois-to-areas</code><br/>
 * Artificial areas created by
 * multipolygon relation processing are not used for POI creation. The location of the POI
 * is determined in different ways.<br/>
 * Element is of type {@link Way}:
 * <ul>
 * <li>the first node tagged with building=entrance</li>
 * <li>the center point of the area</li>
 * </ul>
 * Element is of type {@link MultiPolygonRelation}:
 * <ul>
 * <li>the node with role=label</li>
 * <li>the center point of the biggest area</li>
 * </ul>
 * Each node created is tagged with mkgmap:area2poi=true.<br/>
 * <br/>
 * <code>add-pois-to-lines</code><br/>
 * Adds POIs to lines. Each POI is tagged with mkgmap:line2poi=true.<br/>
 * The following POIs are created for each line:
 * <ul>
 * <li>mkgmap:line2poitype=start: The first point of the line</li>
 * <li>mkgmap:line2poitype=end: The last point of the line</li>
 * <li>mkgmap:line2poitype=inner: Each inner point of the line</li>
 * <li>mkgmap:line2poitype=mid: POI at the middle distance of the line</li>
 * </ul>
 * @author WanMil
 */

public class POIGeneratorHook implements OsmReadingHooks {
        private static final Logger log = Logger.getLogger(POIGeneratorHook.class);

        private List<Entry<String,String>> poiPlacementTags;
        /**
         * maps only those locations which are used in nodes with tags which are used in
         * the points rules with the {@code FROM_NODE_PREFIX} and which are not already {@link CoordPOI} instances.
         * The mapping is only needed to create the POIs, thus we don't create {@link CoordPOI} instances for them.
         */

        private IdentityHashMap<Coord, Node> coordToNodeMap;   
       
        private ElementSaver saver;
       
        private boolean poisToAreas = false;
        private boolean poisToLines = false;
        private boolean poisToLinesStart = false;
        private boolean poisToLinesEnd = false;
        private boolean poisToLinesMid = false;
        private boolean poisToLinesOther = false;
        private NameFinder nameFinder;
        private AreaSizeFunction areaSizeFunction = new AreaSizeFunction();

        private Set<String> usedTagsPOI;
       
        /** Name of the bool tag that is set to true if a POI is created from an area */
        public static final short TKM_AREA2POI = TagDict.getInstance().xlate("mkgmap:area2poi");
        public static final short TKM_LINE2POI = TagDict.getInstance().xlate("mkgmap:line2poi");
        public static final short TKM_LINE2POI_TYPE = TagDict.getInstance().xlate("mkgmap:line2poitype");
        public static final short TKM_WAY_LENGTH = TagDict.getInstance().xlate("mkgmap:way-length");
       
        @Override
        public boolean init(ElementSaver saver, EnhancedProperties props, Style style) {
                poisToAreas = props.containsKey("add-pois-to-areas");
                poisToLines = props.containsKey("add-pois-to-lines");
                if (poisToLines) {
                        String[] opts = {"all"};
                        if (!props.getProperty("add-pois-to-lines").isEmpty()) {
                                opts = props.getProperty("add-pois-to-lines").split(",");
                        }
                       
                        for (String opt : opts) {
                                switch (opt.trim()) {
                                case "start":
                                        poisToLinesStart = true;
                                        break;
                                case "end":
                                        poisToLinesEnd = true;
                                        break;
                                case "mid":
                                        poisToLinesMid = true;
                                        break;
                                case "other":
                                        poisToLinesOther= true;
                                        break;
                                case "all":
                                        poisToLinesStart= true;
                                        poisToLinesEnd= true;
                                        poisToLinesMid = true;
                                        poisToLinesOther= true;
                                        break;

                                default:
                                        throw new IllegalArgumentException("Invalied argument '"+opt+"' for add-pois-to-lines");
                                }
                        }
                       
                }
               
                if (!(poisToAreas || poisToLines)) {
                        log.info("Disable Areas2POIHook because add-pois-to-areas and add-pois-to-lines option is not set.");
                        return false;
                }
                nameFinder = new NameFinder(props);

                this.poiPlacementTags = getPoiPlacementTags(props);
               
                this.saver = saver;
                if (style != null && style.getUsedTagsPOI() != null) {
                        // extract special tags used in the points file
                        usedTagsPOI = style.getUsedTagsPOI().stream()
                                        .filter(s -> s.startsWith(FROM_NODE_PREFIX))
                                        .map(s -> s.substring(POIGeneratorHook.FROM_NODE_PREFIX.length()))
                                        .collect(Collectors.toSet());
                } else {
                        usedTagsPOI = Collections.emptySet();
                }
                return true;
        }
       
        /**
         * Reads the tag definitions of the option poi2area-placement-tags from the given properties.
         * @param props mkgmap options
         * @return the parsed tag definition list
         */

        public static List<Entry<String,String>> getPoiPlacementTags(EnhancedProperties props) {
                if (!props.containsKey("add-pois-to-areas")) {
                        return Collections.emptyList();
                }
               
                List<Entry<String,String>> tagList = new ArrayList<>();
               
                String placementDefs = props.getProperty("pois-to-areas-placement", "entrance=main;entrance=yes;building=entrance");
                placementDefs = placementDefs.trim();
               
                if (placementDefs.length() == 0) {
                        // the POIs should be placed in the center only
                        // => return an empty list
                        return tagList;
                }
               
                String[] placementDefsParts = placementDefs.split(";");
                for (String placementDef : placementDefsParts) {
                        int ind = placementDef.indexOf('=');
                        String tagName = null;
                        String tagValue = null;
                        if (ind < 0) {
                                // only the tag is defined => interpret it as tag=*
                                tagName = placementDef;
                                tagValue = null;
                        } else if (ind > 0) {
                                tagName = placementDef.substring(0,ind);
                                tagValue = placementDef.substring(ind+1);
                        } else {
                                log.error("Option pois-to-areas-placement contains a tag that starts with '='. This is not allowed. Ignoring it.");
                                continue;
                        }
                        tagName = tagName.trim();
                        if (tagName.length() == 0) {
                                log.error("Option pois-to-areas-placement contains a whitespace tag  '='. This is not allowed. Ignoring it.");
                                continue;
                        }
                        if (tagValue != null) {
                                tagValue = tagValue.trim();
                                if (tagValue.length() == 0 || "*".equals(tagValue)) {
                                        tagValue = null;
                                }
                        }
                        Entry<String,String> tag = new AbstractMap.SimpleImmutableEntry<>(tagName, tagValue);
                        tagList.add(tag);
                }
                return tagList;
        }
       
        @Override
        public Set<String> getUsedTags() {
                return poiPlacementTags.stream().map(Map.Entry::getKey).collect(Collectors.toSet());
        }
       
        @Override
        public void end() {
                log.info(getClass().getSimpleName(), "started");
                coordToNodeMap = new IdentityHashMap<>();
                if (!usedTagsPOI.isEmpty()) {
                        for (Node n : saver.getNodes().values()) {
                                if (n.getLocation() instanceof CoordPOI)
                                        continue;
                                for (String key : usedTagsPOI) {
                                        if (n.getTag(key) != null) {
                                                coordToNodeMap.put(n.getLocation(), n);
                                                break;
                                        }
                                }
                        }
                }
                addPOIsForWays();
                addPOIsForMPs();
                coordToNodeMap.clear();
                log.info(getClass().getSimpleName(), "finished");
        }
       
        private int getPlacementOrder(Element elem) {
                for (int order = 0; order < poiPlacementTags.size(); order++) {
                        Entry<String,String> poiTagDef = poiPlacementTags.get(order);
                        String tagValue = elem.getTag(poiTagDef.getKey());
                        if (tagValue != null && (poiTagDef.getValue() == null || poiTagDef.getValue().equals(tagValue))) {
                                return order;
                        }
                }
                // no poi tag match
                return -1;
        }
       
        private void addPOIsForWays() {
                Map<Coord, Integer> labelCoords = new IdentityHashMap<>();
               
                // save all coords with one of the placement tags to a map
                // so that ways use this coord as its labeling point
                if (!poiPlacementTags.isEmpty() && poisToAreas) {
                        for (Node n : saver.getNodes().values()) {
                                int order = getPlacementOrder(n);
                                if (order >= 0) {
                                        Integer prevOrder = labelCoords.get(n.getLocation());
                                        if (prevOrder == null || order < prevOrder.intValue())
                                                labelCoords.put(n.getLocation(), order);
                                }
                        }
                }
               
                log.debug("Found", labelCoords.size(), "label coords");
               
                int ways2POI = 0;
                int lines2POI = 0;
               
                for (Way w : saver.getWays().values()) {
                        // check if way has any tags
                        if (w.getTagCount() == 0) {
                                continue;
                        }

                        // do not add POIs for polygons created by multipolygon processing
                        if (w.tagIsLikeYes(MultiPolygonRelation.TKM_MP_CREATED)) {
                                if (log.isDebugEnabled())
                                        log.debug("MP processed: Do not create POI for", w.toTagString());
                                continue;
                        }
                       
                       
                        // check if it is an area
                        if (w.hasIdenticalEndPoints()) {
                                if (poisToAreas) {
                                        addPOItoPolygon(w, labelCoords);
                                        ways2POI++;
                                }
                        } else {
                                if (poisToLines) {
                                        lines2POI += addPOItoLine(w);
                                }
                        }
                }
               
                if (poisToAreas)
                        log.info(ways2POI, "POIs from single areas created");
                if (poisToLines)
                        log.info(lines2POI, "POIs from lines created");
        }
       
        private void addPOItoPolygon(Way polygon, Map<Coord, Integer> labelCoords) {
                if (!poisToAreas) {
                        return;
                }
               
                // get the coord where the poi is placed
                Coord poiCoord = null;
                // do we have some labeling coords?
                if (!labelCoords.isEmpty()) {
                        int poiOrder = Integer.MAX_VALUE;
                        // go through all points of the way and check if one of the coords
                        // is a labeling coord
                        for (Coord c : polygon.getPoints()) {
                                Integer cOrder = labelCoords.get(c);
                                if (cOrder != null && cOrder.intValue() < poiOrder) {
                                        // this coord is a labelling coord
                                        // use it for the current way
                                        poiCoord = c;
                                        poiOrder = cOrder;
                                        if (poiOrder == 0) {
                                                // there is no higher order
                                                break;
                                        }
                                }
                        }
                }
                if (poiCoord == null) {
                        // did not find any label coord
                        // use the common center point of the area
                        poiCoord = polygon.getCofG();
                }
                // add tag mkgmap:cache_area_size to the original polygon so that it is copied to the POI
                areaSizeFunction.value(polygon);
                addPOI(polygon, poiCoord, TKM_AREA2POI, 0);
        }
       
       
        private int addPOItoLine(Way line) {
                // calculate the middle of the line
                Coord prevC = null;
                double sumDist = 0.0;
                ArrayList<Double> dists = new ArrayList<>(line.getPoints().size()-1);
                for (Coord c : line.getPoints()) {
                        if (prevC != null) {
                                double dist = prevC.distance(c);
                                dists.add(dist);
                                sumDist+=dist;
                        }
                        prevC = c;
                }
               
                int countPOIs = 0;
                if (poisToLinesStart) {
                        Node startNode = addPOI(line, line.getFirstPoint(), TKM_LINE2POI, sumDist);
                        startNode.addTag(TKM_LINE2POI_TYPE, "start");
                        countPOIs++;
                }

                if (poisToLinesEnd) {
                        Node endNode = addPOI(line, line.getLastPoint(), TKM_LINE2POI, sumDist);
                        endNode.addTag(TKM_LINE2POI_TYPE, "end");
                        countPOIs++;
                }
               
                if (poisToLinesOther && line.getPoints().size() > 2) {
                        Coord lastPoint = line.getFirstPoint();
                        for (Coord inPoint : line.getPoints().subList(1, line.getPoints().size() - 1)) {
                                if (inPoint.equals(lastPoint)) {
                                        continue;
                                }
                                lastPoint = inPoint;
                                Node innerNode = addPOI(line, inPoint, TKM_LINE2POI, sumDist);
                                innerNode.addTag(TKM_LINE2POI_TYPE, "inner");
                                countPOIs++;
                        }
                }
                if (poisToLinesMid) {
                        Coord midPoint = null;
                        double remMidDist = sumDist / 2;
                        for (int midPos = 0; midPos < dists.size(); midPos++) {
                                double nextDist = dists.get(midPos);
                                if (remMidDist <= nextDist) {
                                        double frac = remMidDist / nextDist;
                                        midPoint = line.getPoints().get(midPos).makeBetweenPoint(line.getPoints().get(midPos + 1), frac);
                                        break;
                                }
                                remMidDist -= nextDist;
                        }

                        if (midPoint != null) {
                                Node midNode = addPOI(line, midPoint, TKM_LINE2POI, sumDist);
                                midNode.addTag(TKM_LINE2POI_TYPE, "mid");
                                countPOIs++;
                        }
                }
                return countPOIs;

        }
       
        /** Prefix that is added to tags which are copied from the original node. */
        public static final String FROM_NODE_PREFIX = "mkgmap:from-node:";

        private Node addPOI(Element source, Coord poiCoord, short poiTypeTagKey, double wayLength) {
                Node poi = new Node(source.getOriginalId(), poiCoord);
                poi.markAsGeneratedFrom(source);
                poi.copyTags(source);
                poi.deleteTag(MultiPolygonRelation.STYLE_FILTER_TAG);
                poi.addTag(poiTypeTagKey, "true");
                if (poiTypeTagKey == TKM_LINE2POI) {
                        poi.addTag(TKM_WAY_LENGTH, String.valueOf(Math.round(wayLength)));
                }
               
                Node node = null;
                if (poiCoord instanceof CoordPOI) {
                        node = ((CoordPOI) poiCoord).getNode();
                } else {
                        node = coordToNodeMap.get(poiCoord);
                }
                if (node != null) {
                        // add the original tags of the node with the prefix mkgmap:from-node:
                        for (Entry<String, String> entry : node.getTagEntryIterator()) {
                                if (!entry.getKey().startsWith("mkgmap:")) {
                                        poi.addTag(FROM_NODE_PREFIX + entry.getKey(), entry.getValue());
                                }
                        }
                }

                if (log.isDebugEnabled()) {
                        log.debug("Create POI",poi.toTagString(),"from",source.getId(),source.toTagString());
                }
                saver.addNode(poi);
                return poi;
               
        }

        private void addPOIsForMPs() {
                int mps2POI = 0;
                for (Relation r : saver.getRelations().values()) {
                       
                        // create POIs for multipolygon relations only
                        if (!(r instanceof MultiPolygonRelation)) {
                                continue;
                        }
                        Node adminCentre = null;
                        Node labelPOI = null;
                        String relName = nameFinder.getName(r);
                        if (relName != null){
                                for (Entry<String, Element> pair : r.getElements()){
                                        String role = pair.getKey();
                                        Element el = pair.getValue();
                                        if (el instanceof Node){
                                                if ("admin_centre".equals(role)){
                                                        if ("boundary".equals(r.getTag("type")) && "administrative".equals(r.getTag("boundary"))){
                                                                // boundary relations may have a node with role admin_centre, if yes, use the
                                                                // location of it
                                                                String pName = nameFinder.getName(el);
                                                                if (relName.equals(pName) || pName == null){
                                                                        adminCentre = (Node) el;
                                                                        if (log.isDebugEnabled())
                                                                                log.debug("using admin_centre node as location for POI for rel",r.getId(),relName,"at",((Node) el).getLocation());
                                                                }
                                                        }
                                                } else if ("label".equals(role)){
                                                        String label = nameFinder.getName(el);
                                                        if (relName.equals(label) || label == null){
                                                                labelPOI = (Node) el;
                                                                log.debug("using label node as location for POI for rel", r.getId(), relName, "at", ((Node) el).getLocation());
                                                                break;
                                                        } else {
                                                                log.warn("role label", el.toBrowseURL(), label, "is ignored because it has a different name than" ,r.toBrowseURL(), relName);
                                                        }
                                                }
                                        }
                                }
                        }
                        Coord point = null;
                        if (adminCentre == null && labelPOI == null)
                                point = ((MultiPolygonRelation)r).getCofG();
                        else {
                                if (labelPOI != null)
                                        point = labelPOI.getLocation();
                                else
                                        point = adminCentre.getLocation();
                        }

                        if (point != null) {
                                Node poi = addPOI(r, point, TKM_AREA2POI, 0);
                                // remove the type tag which makes only sense for relations
                                poi.deleteTag("type");
                                mps2POI++;
                        }
                }
                log.info(mps2POI,"POIs from multipolygons created");
        }


}