All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.graphhopper.util.PathSimplification Maven / Gradle / Ivy

Go to download

GraphHopper is a fast and memory efficient Java road routing engine working seamlessly with OpenStreetMap data.

There is a newer version: 10.0
Show newest version
/*
 *  Licensed to GraphHopper GmbH under one or more contributor
 *  license agreements. See the NOTICE file distributed with this work for
 *  additional information regarding copyright ownership.
 *
 *  GraphHopper GmbH licenses this file to you under the Apache License,
 *  Version 2.0 (the "License"); you may not use this file except in
 *  compliance with the License. You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.graphhopper.util;

import com.graphhopper.ResponsePath;
import com.graphhopper.util.details.PathDetail;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * This class simplifies the path, using {@link RamerDouglasPeucker}, but also considers a given list of partitions of
 * the path. Each partition separates the points of the path into non-overlapping intervals and the simplification is
 * done such that we never simplify across the boundaries of these intervals. This is important, because the points
 * at the interval boundaries must not be removed, e.g. when they are referenced by instructions.
 * For example for a path with twenty points and three partitions like this
 * 

* - (0,1,2,3)(3,4)(4,4)(4,5,6,7)(7,8,9,10,11,12)(12,13,14,15,16)(17,18,19) * - (0,1)(1,2,3,4)(4,5,6,7)(7,7)(8,9,10,11)(12,13,14,15)(16,17,18,19) * - (0,1,2,3,4,5)(6,7,8,9,10,11,12,13,14),(14,15,16,17,18)(18,18)(18,19) *

* we run the simplification for the following intervals: *

* (0,1)(1,2,3)(3,4)(4,5)(5,6,7)(7,8,9,10,11)(11,12)(12,13,14)(14,15)(15,16)(16,17,18)(18,19) * * @author Robin Boldt * @author easbar */ public class PathSimplification { private final PointList pointList; /** * @see PathSimplification */ private final List partitions; private final RamerDouglasPeucker ramerDouglasPeucker; // temporary variables used when traversing the different partitions private final int numPartitions; private final int[] currIntervalIndex; private final int[] currIntervalStart; private final int[] currIntervalEnd; private final boolean[] partitionFinished; // keep track of how many points were removed by Ramer-Douglas-Peucker in the current and previous intervals private final int[] removedPointsInCurrInterval; private final int[] removedPointsInPrevIntervals; /** * Convenience method used to obtain the partitions from a calculated path with details and instructions */ public static PointList simplify(ResponsePath responsePath, RamerDouglasPeucker ramerDouglasPeucker, boolean enableInstructions) { final PointList pointList = responsePath.getPoints(); List partitions = new ArrayList<>(); // make sure all waypoints are retained in the simplified point list // we copy the waypoint indices into temporary intervals where they will be mutated by the simplification, // afterwards we need to update the way point indices accordingly. List intervals = new ArrayList<>(); for (int i = 0; i < responsePath.getWaypointIndices().size() - 1; i++) intervals.add(new Interval(responsePath.getWaypointIndices().get(i), responsePath.getWaypointIndices().get(i + 1))); partitions.add(new Partition() { @Override public int size() { return intervals.size(); } @Override public int getIntervalLength(int index) { return intervals.get(index).end - intervals.get(index).start; } @Override public void setInterval(int index, int start, int end) { intervals.get(index).start = start; intervals.get(index).end = end; } }); // todo: maybe this code can be simplified if path details and instructions would be merged, see #1121 if (enableInstructions) { final InstructionList instructions = responsePath.getInstructions(); partitions.add(new Partition() { @Override public int size() { return instructions.size(); } @Override public int getIntervalLength(int index) { return instructions.get(index).getLength(); } @Override public void setInterval(int index, int start, int end) { Instruction instruction = instructions.get(index); if (instruction instanceof ViaInstruction || instruction instanceof FinishInstruction) { if (start != end) { throw new IllegalStateException("via- and finish-instructions are expected to have zero length"); } // have to make sure that via instructions and finish instructions contain a single point // even though their 'instruction length' is zero. end++; } instruction.setPoints(pointList.shallowCopy(start, end, false)); } }); } for (final Map.Entry> entry : responsePath.getPathDetails().entrySet()) { // If the pointList only contains one point, PathDetails have to be empty because 1 point => 0 edges final List detail = entry.getValue(); if (detail.isEmpty() && pointList.size() > 1) throw new IllegalStateException("PathDetails " + entry.getKey() + " must not be empty"); partitions.add(new Partition() { @Override public int size() { return detail.size(); } @Override public int getIntervalLength(int index) { return detail.get(index).getLength(); } @Override public void setInterval(int index, int start, int end) { PathDetail pd = detail.get(index); pd.setFirst(start); pd.setLast(end); } }); } simplify(responsePath.getPoints(), partitions, ramerDouglasPeucker); List simplifiedWaypointIndices = new ArrayList<>(); simplifiedWaypointIndices.add(intervals.get(0).start); for (Interval interval : intervals) simplifiedWaypointIndices.add(interval.end); responsePath.setWaypointIndices(simplifiedWaypointIndices); assertConsistencyOfPathDetails(responsePath.getPathDetails()); if (enableInstructions) assertConsistencyOfInstructions(responsePath.getInstructions(), responsePath.getPoints().size()); return pointList; } public static void simplify(PointList pointList, List partitions, RamerDouglasPeucker ramerDouglasPeucker) { new PathSimplification(pointList, partitions, ramerDouglasPeucker).simplify(); } private PathSimplification(PointList pointList, List partitions, RamerDouglasPeucker ramerDouglasPeucker) { this.pointList = pointList; this.partitions = partitions; this.ramerDouglasPeucker = ramerDouglasPeucker; numPartitions = this.partitions.size(); currIntervalIndex = new int[numPartitions]; currIntervalStart = new int[numPartitions]; currIntervalEnd = new int[numPartitions]; partitionFinished = new boolean[numPartitions]; removedPointsInCurrInterval = new int[numPartitions]; removedPointsInPrevIntervals = new int[numPartitions]; } private void simplify() { if (pointList.size() <= 2) { pointList.makeImmutable(); return; } // no partitions -> no constraints, just simplify the entire point list if (partitions.isEmpty()) { ramerDouglasPeucker.simplify(pointList, 0, pointList.size() - 1); pointList.makeImmutable(); return; } // Ramer-Douglas-Peucker never removes the first/last point of a given interval, so as long as we only run it // on each interval we can be sure that the interval boundaries will remain in the point list. // Whenever we remove points from an interval we have to update the interval indices of all partitions. // For example if an interval goes from point 4 to 9 and we remove points 5 and 7 we have to update the interval // to [4,7]. // The basic idea to do this is as follows: We iterate through the point list and whenever we hit an interval // end (q) in one of the partitions we run Ramer-Douglas-Peucker for the interval [p,q], where p is the point where // the last interval ended. We keep track of the number of removed points in the current and previous intervals // to be able to calculate the updated indices. // prepare for the first interval in each partition int intervalStart = 0; for (int i = 0; i < numPartitions; i++) { currIntervalEnd[i] = this.partitions.get(i).getIntervalLength(currIntervalIndex[i]); } // iterate the point list and simplify and update the intervals on the go for (int p = 0; p < pointList.size(); p++) { int removed = 0; // first we check if we hit the end of an interval for one of the partitions and run Ramer-Douglas-Peucker if we do for (int s = 0; s < numPartitions; s++) { if (partitionFinished[s]) { continue; } if (p == currIntervalEnd[s]) { // This is important for performance: we must not compress the point list after each call to // simplify, otherwise a lot of data is copied, especially for long routes (e.g. many via nodes), // see #1764. Note that since the point list does not get compressed here yet we have to keep track // of the total number of removed points to calculate the new interval boundaries later final boolean compress = false; removed = ramerDouglasPeucker.simplify(pointList, intervalStart, currIntervalEnd[s], compress); intervalStart = p; break; } } // now we have (possibly) removed some points we need to update the current intervals in all partitions for (int s = 0; s < numPartitions; s++) { if (partitionFinished[s]) { continue; } removedPointsInCurrInterval[s] += removed; // if the current interval of this partition ends at p, we update the interval boundaries. there is // just a special catch: there can be multiple consecutive intervals that end with p, because there // are intervals with a single point, for example p=3 and a partition=[0,3][3,3][3,3] boolean nextIntervalHasOnlyOnePoint; do { if (p == currIntervalEnd[s]) { nextIntervalHasOnlyOnePoint = updateInterval(p, s); } else { break; } } while (nextIntervalHasOnlyOnePoint); } } // now we finally have to compress the pointList (actually remove the deleted points). note only after this // call the (now shifted) indices in path details and instructions are correct RamerDouglasPeucker.removeNaN(pointList); // Make sure that the instruction references are not broken pointList.makeImmutable(); assertConsistencyOfIntervals(); } /** * @param p point index * @param s partition index */ private boolean updateInterval(int p, int s) { boolean nextIntervalHasOnlyOnePoint = false; // update interval boundaries final int updatedStart = currIntervalStart[s] - removedPointsInPrevIntervals[s]; final int updatedEnd = currIntervalEnd[s] - removedPointsInPrevIntervals[s] - removedPointsInCurrInterval[s]; this.partitions.get(s).setInterval(currIntervalIndex[s], updatedStart, updatedEnd); // update the removed point counters removedPointsInPrevIntervals[s] += removedPointsInCurrInterval[s]; removedPointsInCurrInterval[s] = 0; // prepare for the next interval currIntervalIndex[s]++; currIntervalStart[s] = p; if (currIntervalIndex[s] >= this.partitions.get(s).size()) { partitionFinished[s] = true; } else { int length = this.partitions.get(s).getIntervalLength(currIntervalIndex[s]); currIntervalEnd[s] += length; // special case at via points etc. if (length == 0) { nextIntervalHasOnlyOnePoint = true; } } return nextIntervalHasOnlyOnePoint; } private void assertConsistencyOfIntervals() { final int expected = pointList.size() - 1; for (int i = 0; i < partitions.size(); i++) { final Partition partition = partitions.get(i); int count = 0; for (int j = 0; j < partition.size(); j++) { count += partition.getIntervalLength(j); } if (count != expected) { throw new IllegalStateException("Simplified intervals are inconsistent: " + count + " vs. " + expected + " for intervals with index: " + i); } } } private static void assertConsistencyOfPathDetails(Map> pathDetails) { for (Map.Entry> pdEntry : pathDetails.entrySet()) { List list = pdEntry.getValue(); if (list.isEmpty()) continue; PathDetail prevPD = list.get(0); for (int i = 1; i < list.size(); i++) { if (prevPD.getLast() != list.get(i).getFirst()) throw new IllegalStateException("PathDetail list " + pdEntry.getKey() + " is inconsistent due to entries " + prevPD + " vs. " + list.get(i)); prevPD = list.get(i); } } } private static void assertConsistencyOfInstructions(InstructionList instructions, int numPoints) { // the total length of the instruction intervals must match the length of the point list. // todo: it would be even better to make sure each instruction interval starts where the previous one ended, but // currently instructions do not offer this int expected = numPoints - 1; int count = 0; for (Instruction instruction : instructions) { count += instruction.getLength(); } if (count != expected) { throw new IllegalArgumentException("inconsistent instructions, total interval length: " + count + " vs. point list length " + expected); } } /** * Represents a partition of a {@link PointList} into consecutive intervals, for example a list with six points * can be partitioned into something like [0,2],[2,2],[2,3][3,5]. Note that intervals with a single point are * allowed, but each interval must start where the previous one ended. */ interface Partition { int size(); // todo: it would be nice to be able to retrieve the actual start and end of each interval to make the // code here more straight-forward, but currently instructions only offer the length of the interval int getIntervalLength(int index); void setInterval(int index, int start, int end); } public static class Interval { public int start; public int end; public Interval(int start, int end) { this.start = start; this.end = end; } @Override public String toString() { return "[" + start + ", " + end + "]"; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy