ORSOSMReader.java

/*  This file is part of Openrouteservice.
 *
 *  Openrouteservice is free software; you can redistribute it and/or modify it under the terms of the
 *  GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1
 *  of the License, or (at your option) any later version.

 *  This library 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 Lesser General Public License for more details.

 *  You should have received a copy of the GNU Lesser General Public License along with this library;
 *  if not, see <https://www.gnu.org/licenses/>.
 */
package org.heigit.ors.routing.graphhopper.extensions;

import com.carrotsearch.hppc.LongArrayList;
import com.graphhopper.reader.ReaderNode;
import com.graphhopper.reader.ReaderWay;
import com.graphhopper.reader.osm.OSMReader;
import com.graphhopper.storage.GraphHopperStorage;
import com.graphhopper.storage.IntsRef;
import com.graphhopper.util.EdgeIteratorState;
import com.graphhopper.util.shapes.GHPoint;
import org.apache.log4j.Logger;
import org.heigit.ors.routing.graphhopper.extensions.reader.osmfeatureprocessors.OSMFeatureFilter;
import org.heigit.ors.routing.graphhopper.extensions.reader.osmfeatureprocessors.WheelchairWayFilter;
import org.heigit.ors.routing.graphhopper.extensions.storages.builders.*;
import org.locationtech.jts.geom.Coordinate;

import java.io.InvalidObjectException;
import java.util.*;
import java.util.Map.Entry;

public class ORSOSMReader extends OSMReader {

    private static final Logger LOGGER = Logger.getLogger(ORSOSMReader.class.getName());

    private final GraphProcessContext procCntx;
    private boolean processNodeTags;
    private final OSMDataReaderContext readerCntx;

    private final HashMap<Long, HashMap<String, String>> nodeTags = new HashMap<>();

    private boolean processGeom = false;
    private boolean processSimpleGeom = false;
    private boolean processWholeGeom = false;
    private boolean detachSidewalksFromRoad = false;

    private final boolean getElevationFromPreprocessedData;
    private boolean getElevationFromPreprocessedDataErrorLogged = false;

    private final List<OSMFeatureFilter> filtersToApply = new ArrayList<>();

    private final HashSet<String> extraTagKeys;

    public ORSOSMReader(GraphHopperStorage storage, GraphProcessContext procCntx) {
        super(storage);

        enforce2D();
        this.procCntx = procCntx;
        this.procCntx.initArrays();
        this.readerCntx = new OSMDataReaderContext(this);
        getElevationFromPreprocessedData = procCntx.getElevationFromPreprocessedData();

        initNodeTagsToStore(new HashSet<>(Arrays.asList("maxheight", "maxweight", "maxweight:hgv", "maxwidth", "maxlength", "maxlength:hgv", "maxaxleload")));
        extraTagKeys = new HashSet<>();
        // Look if we should do border processing - if so then we have to process the geometry
        for (GraphStorageBuilder b : this.procCntx.getStorageBuilders()) {
            if (b instanceof BordersGraphStorageBuilder) {
                this.processGeom = true;
            }

            if (b instanceof HereTrafficGraphStorageBuilder) {
                this.processGeom = true;
                this.processWholeGeom = true;
            }

            if (b instanceof WheelchairGraphStorageBuilder) {
                filtersToApply.add(new WheelchairWayFilter());
                this.processNodeTags = true;
                this.detachSidewalksFromRoad = true;
                this.processSimpleGeom = true;
                extraTagKeys.add("kerb");
                extraTagKeys.add("kerb:both");
                extraTagKeys.add("kerb:left");
                extraTagKeys.add("kerb:right");
                extraTagKeys.add("kerb:height");
                extraTagKeys.add("kerb:both:height");
                extraTagKeys.add("kerb:left:height");
                extraTagKeys.add("kerb:right:height");
            }

            if (b instanceof RoadAccessRestrictionsGraphStorageBuilder) {
                this.processNodeTags = true;
                extraTagKeys.add("access");
                extraTagKeys.add("bicycle");
                extraTagKeys.add("foot");
                extraTagKeys.add("horse");
                extraTagKeys.add("motor_vehicle");
                extraTagKeys.add("motorcar");
                extraTagKeys.add("motorcycle");
            }
        }
    }

    @Override
    public ReaderNode onProcessNode(ReaderNode node) {
        // On OSM, nodes are seperate entities which are used to make up ways. So basically, a node is read before a
        // way and if it has some properties that could affect routing, these properties need to be stored so that they
        // can be accessed when it comes to using ways
        if (processNodeTags && node.hasTags()) {
            // Check each node and store the tags that are required
            HashMap<String, String> tagValues = new HashMap<>();
            Set<String> nodeKeys = node.getTags().keySet();
            for (String key : nodeKeys) {
                if (extraTagKeys.contains(key)) {
                    tagValues.put(key, node.getTag(key));
                }
            }

            // Now if we have tag data, we need to store it
            if (tagValues.size() > 0) {
                nodeTags.put(node.getId(), tagValues);
            }
        }
        return node;
    }

    @Override
    protected void processWay(ReaderWay way) {
        // As a first step we need to check to see if we should try to split the way
        if (this.detachSidewalksFromRoad) {
            // If we are requesting to split sidewalks, then we need to create multiple ways from a single road
            // For example, if a road way has been tagged as having sidewalks on both sides (sidewalk=both), then we
            // need to create two ways - one for the left sidewalk and one for the right. The Graph Builder would then
            // process these ways separately so that additional edges are created in the graph.

            for (OSMFeatureFilter filter : filtersToApply) {
                try {
                    filter.assignFeatureForFiltering(way);
                } catch (InvalidObjectException ioe) {
                    LOGGER.error("Invalid object for filtering - " + ioe.getMessage());
                }

                if (filter.accept()) {
                    // We can only perform the processing of the ways here and so we cannot delegate it to another object.
                    while (!filter.isWayProcessingComplete()) {
                        filter.prepareForProcessing();
                        super.processWay(way);
                    }
                }
            }

            return;

        }

        // Normal processing
        super.processWay(way);
    }

    /**
     * Method to be run against each way obtained from the data. If one of the storage builders needs geometry
     * determined in the constructor then we need to get the geometry as well as the tags.
     * Also we need to pass through any important tag values obtained from nodes through to the processing stage so
     * that they can be evaluated.
     *
     * @param way The way object read from the OSM data (not including geometry)
     */
    @Override
    public void onProcessWay(ReaderWay way) {

        Map<Integer, Map<String, String>> tags = new HashMap<>();
        ArrayList<Coordinate> coords = new ArrayList<>();
        ArrayList<Coordinate> allCoordinates = new ArrayList<>();

        if (processNodeTags) {
            // If we are processing the node tags then we need to obtain the tags for nodes that are on the way. We
            // should store the internal node id though rather than the osm node as during the edge processing, we
            // do not know the osm node id

            LongArrayList osmNodeIds = way.getNodes();
            int size = osmNodeIds.size();

            for (int i = 0; i < size; i++) {
                // find the node
                long id = osmNodeIds.get(i);
                // replace the osm id with the internal id
                int internalId = getNodeMap().get(id);
                HashMap<String, String> tagsForNode = nodeTags.get(id);

                if (tagsForNode != null) {
                    tags.put(internalId, nodeTags.get(id));
                }
            }
        }

        if (processGeom || processSimpleGeom) {
            // We need to pass the geometry of the way aswell as the ReaderWay object
            // This is slower so should only be done when needed

            // First we need to generate the geometry
            LongArrayList osmNodeIds = new LongArrayList();
            LongArrayList allOsmNodes = way.getNodes();

            if (allOsmNodes.size() > 1) {
                if (processSimpleGeom) {
                    // We only want the start and end nodes
                    osmNodeIds.add(allOsmNodes.get(0));
                    osmNodeIds.add(allOsmNodes.get(allOsmNodes.size() - 1));
                } else {
                    // Process all nodes
                    osmNodeIds = allOsmNodes;
                }
            }

            if (osmNodeIds.size() > 1) {

                for (int i = 0; i < osmNodeIds.size(); i++) {
                    int id = getNodeMap().get(osmNodeIds.get(i));
                    try {
                        double lat = getLatitudeOfNode(id, false);
                        double lon = getLongitudeOfNode(id, false);
                        boolean validGeometry = !(lat == 0 || lon == 0 || Double.isNaN(lat) || Double.isNaN(lon));
                        if (processWholeGeom && validGeometry) {
                            allCoordinates.add(new Coordinate(getTmpLongitude(id), getTmpLatitude(id)));
                        }
                        // Add the point to the line
                        // Check that we have a tower node
                        lat = getLatitudeOfNode(id, true);
                        lon = getLongitudeOfNode(id, true);
                        if (validGeometry) {
                            coords.add(new Coordinate(lon, lat));
                        }
                    } catch (Exception e) {
                        LOGGER.error("Could not process node " + osmNodeIds.get(i));
                    }
                }
            }

        }

        if (tags.size() > 0 || coords.size() > 1) {
            // Use an overloaded method that allows the passing of parameters from this reader
            procCntx.processWay(way, coords.toArray(new Coordinate[0]), tags, allCoordinates.toArray(new Coordinate[0]));
        } else {
            procCntx.processWay(way);
        }
    }

    /* The following two methods are not ideal, but due to a preprocessing stage of GH they are required if you want
     * the geometry of the whole way. */

    /**
     * Find the latitude of the node with the given ID. It checks to see what type of node it is and then finds the
     * latitude from the correct storage location.
     *
     * @param id Internal ID of the OSM node.
     * @return Return the latitude as double.
     */
    private double getLatitudeOfNode(int id, boolean onlyTower) {
        // for speed, we only want to handle the geometry of tower nodes (those at junctions)
        if (id == EMPTY_NODE)
            return Double.NaN;
        if (id < TOWER_NODE) {
            // tower node
            id = -id - 3;
            return getNodeAccess().getLat(id);
        } else if (id > -TOWER_NODE) {
            // pillar node
            // Do we want to return it if it is not a tower node?
            if (onlyTower) {
                return Double.NaN;
            } else {
                return pillarInfo.getLat(id);
            }
        } else {
            // e.g. if id is not handled from preparse (e.g. was ignored via isInBounds)
            return Double.NaN;
        }
    }

    /**
     * Find the longitude of the node with the given ID. It checks to see what type of node it is and then finds the
     * longitude from the correct storage location.
     *
     * @param id Internal ID of the OSM node
     * @return Return the longitude as double
     */
    private double getLongitudeOfNode(int id, boolean onlyTower) {
        if (id == EMPTY_NODE)
            return Double.NaN;
        if (id < TOWER_NODE) {
            // tower node
            id = -id - 3;
            return getNodeAccess().getLon(id);
        } else if (id > -TOWER_NODE) {
            // pillar node
            // Do we want to return it if it is not a tower node?
            if (onlyTower) {
                return Double.NaN;
            } else {
                return pillarInfo.getLat(id);
            }
        } else {
            // e.g. if id is not handled from preparse (e.g. was ignored via isInBounds)
            return Double.NaN;
        }
    }

    /**
     * Applies tags of nodes that lie on a way onto the way itself so that they are
     * regarded in the following storage building process. E.g. a maxheight tag on a node will
     * be treated like a maxheight tag on the way the node belongs to.
     *
     * @param way the way to process
     */
    @Override
    public void applyNodeTagsToWay(ReaderWay way) {
        LongArrayList osmNodeIds = way.getNodes();
        int size = osmNodeIds.size();
        if (size > 2) {
            // If it is a crossing then we need to apply any kerb tags to the way, but we need to make sure we keep the "worse" one
            for (int i = 1; i < size - 1; i++) {
                long nodeId = osmNodeIds.get(i);
                if (nodeHasTagsStored(nodeId)) {
                    java.util.Iterator<Entry<String, Object>> it = getStoredTagsForNode(nodeId).entrySet().iterator();
                    while (it.hasNext()) {
                        Map.Entry<String, Object> pairs = it.next();
                        String key = pairs.getKey();
                        String value = pairs.getValue().toString();
                        way.setTag(key, value);
                    }
                }
            }
        }
    }


    @Override
    protected void onProcessEdge(ReaderWay way, EdgeIteratorState edge) {
        try {
            // Pass through the coordinates of the graph nodes
            Coordinate baseCoord = new Coordinate(
                    getLongitudeOfNode(edge.getBaseNode(), false),
                    getLatitudeOfNode(edge.getBaseNode(), false)
            );
            Coordinate adjCoordinate = new Coordinate(
                    getLongitudeOfNode(edge.getAdjNode(), false),
                    getLatitudeOfNode(edge.getAdjNode(), false)
            );

            procCntx.processEdge(way, edge, new Coordinate[]{baseCoord, adjCoordinate});
        } catch (Exception ex) {
            LOGGER.warn(ex.getMessage() + ". Way id = " + way.getId());
        }
    }

    @Override
    protected boolean onCreateEdges(ReaderWay way, LongArrayList osmNodeIds, IntsRef wayFlags, List<EdgeIteratorState> createdEdges) {
        try {
            return procCntx.createEdges(readerCntx, way, osmNodeIds, wayFlags, createdEdges);
        } catch (Exception ex) {
            LOGGER.warn(ex.getMessage() + ". Way id = " + way.getId());
        }

        return false;
    }

    @Override
    protected void recordExactWayDistance(ReaderWay way, LongArrayList osmNodeIds) {
        super.recordExactWayDistance(way, osmNodeIds);

        // compute exact way distance for ferries in order to improve travel time estimate, see #1037
        if (way.hasTag("route", "ferry", "shuttle_train")) {
            double totalDist = 0d;
            long nodeId = osmNodeIds.get(0);
            int first = getNodeMap().get(nodeId);
            double firstLat = getTmpLatitude(first);
            double firstLon = getTmpLongitude(first);
            double currLat = firstLat;
            double currLon = firstLon;
            double latSum = currLat;
            double lonSum = currLon;
            int sumCount = 1;
            int len = osmNodeIds.size();
            for (int i = 1; i < len; i++) {
                long nextNodeId = osmNodeIds.get(i);
                int next = getNodeMap().get(nextNodeId);
                double nextLat = getTmpLatitude(next);
                double nextLon = getTmpLongitude(next);
                if (!Double.isNaN(currLat) && !Double.isNaN(currLon) && !Double.isNaN(nextLat) && !Double.isNaN(nextLon)) {
                    latSum = latSum + nextLat;
                    lonSum = lonSum + nextLon;
                    sumCount++;
                    totalDist = totalDist + getDistanceCalc().calcDist(currLat, currLon, nextLat, nextLon);

                    currLat = nextLat;
                    currLon = nextLon;
                }
            }
            if (totalDist > 0) {
                way.setTag("exact_distance", totalDist);
                way.setTag("exact_center", new GHPoint(latSum / sumCount, lonSum / sumCount));
            }
        }
    }

    @Override
    protected void finishedReading() {
        super.finishedReading();
        procCntx.finish();
    }

    @Override
    protected double getElevation(ReaderNode node) {
        if (getElevationFromPreprocessedData) {
            double ele = node.getEle();
            if (Double.isNaN(ele)) {
                if (!getElevationFromPreprocessedDataErrorLogged) {
                    LOGGER.warn("elevation_preprocessed set to true in ors config, still found a Node with invalid ele tag! Set this flag only if you use a preprocessed pbf file! Node ID: " + node.getId());
                    getElevationFromPreprocessedDataErrorLogged = true;
                }
                ele = 0;
            }
            return ele;
        }
        return super.getElevation(node);
    }
}