diff --git a/.gitignore b/.gitignore index 8e537c5b..6660e224 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,7 @@ nbactions.xml .project matching-vaadin/src/main/webapp/ matching-core/build.xml -/matching-core/nbproject/ \ No newline at end of file +/map-data/**/* +bin/ +measurement* +/matching-core/nbproject/ diff --git a/map-matching.sh b/map-matching.sh index 9db4ead4..236b6a0e 100755 --- a/map-matching.sh +++ b/map-matching.sh @@ -1,53 +1,128 @@ #!/bin/bash +# bail if any errors ... +set -e + if [ "$JAVA_HOME" != "" ]; then - JAVA=$JAVA_HOME/bin/java + JAVA=$JAVA_HOME/bin/java fi if [ "$JAVA" = "" ]; then - JAVA=java + JAVA=java fi echo "using $JAVA" +function set_jar { + local module=$1 + local pattern="matching-$module/target/graphhopper-map-matching-*-dependencies.jar" + if ! ls $pattern > /dev/null 2>&1; then + mvn --projects hmm-lib -DskipTests=true install + mvn --projects matching-$module,matching-core -DskipTests=true install assembly:single + fi + JAR=$(ls matching-$module/target/graphhopper-map-matching-*-dependencies.jar) +} + if [ "$1" = "action=start-server" ]; then - function set_jar_path { - JAR=$(ls matching-web/target/graphhopper-map-matching-web-*-dependencies.jar) - } - - set_jar_path - - if [ ! -f "$JAR" ]; then - mvn --non-recursive install - mvn -am --projects matching-web -DskipTests=true install - mvn --projects matching-web -DskipTests=true install assembly:single - set_jar_path - fi - - ARGS="graph.location=./graph-cache jetty.resourcebase=matching-web/src/main/webapp" - + set_jar "web" + ARGS="graph.location=./graph-cache jetty.resourcebase=matching-web/src/main/webapp" elif [ "$1" = "action=test" ]; then + export MAVEN_OPTS="-Xmx400m -Xms400m" + mvn clean test verify + # return exit code of mvn + exit $? +elif [ "$1" = "action=measurement" ]; then + # the purpose of this is to run the com.graphhopper.matching.util.Measurement suite, which outputs + # measurement files locally. Usage is either + # + # ./map-matching.sh action=measurement map.osm.pbf + # + # which just compiles the current source and exports the measurement file, or + # + # ./map-matching.sh action=measurement map.osm.pbf 3 + # + # which (in this case) runs the measurement suite for the last 3 commits. (This involves checking them + # out and building one-by-one, which is quite slow.) + # TODO: cache some of the builds/tests to speed things up if the developer's looking for quick feedback + # TODO: figure out a way to genericise this - see GH PR 894 + # TODO: add an error quantification (e.g. std) to the measurement class (inc. in GH), and add it to the outputs (inc. plots) + + set_jar "core" + ARGS="config=$CONFIG graph.location=$GRAPH datareader.file=$2 prepare.ch.weightings=fastest graph.flag_encoders=car prepare.min_network_size=10000 prepare.min_oneway_network_size=10000" + current_branch=$(git rev-parse --abbrev-ref HEAD) + function startMeasurement { + # runs a measurement for the current code base + mvn --projects matching-core -DskipTests=true clean install assembly:single + measurement_fname="measurement$(date +%Y-%m-%d_%H_%M_%S).properties" + "$JAVA" $JAVA_OPTS -cp "$JAR" com.graphhopper.matching.util.Measurement $ARGS measurement.location="$measurement_fname" + } + + # use all versions starting from HEAD + last_commits=$3 + if [ -z "$last_commits" ] || [ "$last_commits" -eq 1 ]; then + startMeasurement + echo "" + cat "$measurement_fname" + exit 0 + else + # as above, let's check out, build, and test each of the last commits. The code looks a bit + # messier than that, as we merge the results into a single file (to make it easier to + # comparem measurements) + combined="measurement_$(git log --format="%h" | head -n $last_commits | tail -n1)_$(git log --format="%h" | head -n1).properties" + tmp_values="$combined.values" + rm -f "$tmp_values" + echo -e "commits:\n--------------------------\n" > "$combined" + commits=$(git rev-list HEAD -n "$last_commits" | tac) # NOTE: tac is to reverse so we start with oldest first + first=true + empty_pad="" + header=$(printf "%30s" "") + subheader=$header + for commit in $commits; do + git checkout "$commit" + git log --format="%h [%cd] %s" --date=short -n 1 >> "$combined" + # do measurement for this commit + startMeasurement + # update headers: + header=$(printf "%s%-20s" "$header" $(echo "$commit" | cut -c1-7)) + subheader=$(printf "%s%-20s" "$subheader" "-------") + # now merge it: + while read -r line; do + key=${line%%=*} + value=$(printf "%-20s" "${line##*=}") + if [ "$first" = true ] ; then + # first commit, so print key and value + printf "%-30s%s\n" "$key" "$value" >> "$tmp_values" + else + if grep "$key" "$tmp_values" > /dev/null; then + # second (or later) commit, in which key already exists, so we simply append it + # TODO: this may fail if e.g. a key exists in commit 1, not in commit 2, and then again in commit 3 ... pretty unlikely + sed -ri "s/($key.*)/\1$value/g" "$tmp_values" + else + # second (or later) commit, in which a new key has appeeard - so add a new line + printf "%-30s%s%s\n" "$key" "$empty_pad" "$value" >> "$tmp_values" + fi + fi + done < "$measurement_fname" + first=false + empty_pad=$(printf "%s%-20s" "$empty_pad" "") + done + echo -e "\nmeasurements:\n-------------\n" >> "$combined" + echo "$header" >> "$combined" + echo "$subheader" >> "$combined" + cat "$tmp_values" >> "$combined" + + # show to user now: + echo "" + cat "$combined" - export MAVEN_OPTS="-Xmx400m -Xms400m" - mvn clean test verify - # return exit code of mvn - exit $? - + # remove tmp file and change back to original branch and then we're done + rm -f '$tmp_values' + git checkout $current_branch + exit 0 + fi else - function set_jar_path { - JAR=$(ls matching-core/target/graphhopper-map-matching-*-dependencies.jar) - } - - set_jar_path - - if [ ! -f "$JAR" ]; then - mvn --non-recursive install - mvn -am --projects matching-core -DskipTests=true install - mvn --projects matching-core -DskipTests=true install assembly:single - set_jar_path - fi - - ARGS="$@" + set_jar "core" + ARGS="$@" fi -exec "$JAVA" $JAVA_OPTS -jar $JAR $ARGS prepare.min_network_size=0 prepare.min_one_way_network_size=0 +exec "$JAVA" $JAVA_OPTS -jar "$JAR" $ARGS prepare.min_network_size=0 prepare.min_one_way_network_size=0 diff --git a/matching-core/src/main/java/com/graphhopper/matching/util/Measurement.java b/matching-core/src/main/java/com/graphhopper/matching/util/Measurement.java new file mode 100644 index 00000000..fbf1505f --- /dev/null +++ b/matching-core/src/main/java/com/graphhopper/matching/util/Measurement.java @@ -0,0 +1,238 @@ +/* + * 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.matching.util; + +import com.graphhopper.GHRequest; +import com.graphhopper.GHResponse; +import com.graphhopper.GraphHopper; +import com.graphhopper.matching.LocationIndexMatch; +import com.graphhopper.matching.MapMatching; +import com.graphhopper.matching.MatchResult; +import com.graphhopper.reader.osm.GraphHopperOSM; +import com.graphhopper.routing.AlgorithmOptions; +import com.graphhopper.routing.util.*; +import com.graphhopper.storage.GraphHopperStorage; +import com.graphhopper.storage.index.LocationIndexTree; +import com.graphhopper.util.*; +import com.graphhopper.util.shapes.BBox; +import com.graphhopper.util.shapes.GHPoint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Random; +import java.util.TreeMap; + +/** + * @author Peter Karisch + * @author kodonnell + */ +public class Measurement { + private static final Logger logger = LoggerFactory.getLogger(Measurement.class); + private final Map properties = new TreeMap(); + private long seed; + private int count; + private BBox bbox; + private DistanceCalcEarth distCalc = new DistanceCalcEarth(); + + public static void main(String[] strs) throws Exception { + new Measurement().start(CmdArgs.read(strs)); + } + + // creates measurement result file in the format = + void start(CmdArgs args) throws Exception { + + // read and initialize arguments: + String graphLocation = args.get("graph.location", ""); + String propLocation = args.get("measurement.location", ""); + if (Helper.isEmpty(propLocation)) { + throw new Exception( + "You must provide an output location via the 'measurement.location' argument"); + } + seed = args.getLong("measurement.seed", 123); + count = args.getInt("measurement.count", 5000); + + // create hopper instance + GraphHopper hopper = new GraphHopperOSM(); + hopper.init(args).forDesktop(); + hopper.getCHFactoryDecorator().setEnabled(true); + hopper.getCHFactoryDecorator().setDisablingAllowed(true); + hopper.importOrLoad(); + + // and map-matching stuff + GraphHopperStorage graph = hopper.getGraphHopperStorage(); + bbox = graph.getBounds(); + LocationIndexMatch locationIndex = new LocationIndexMatch(graph, + (LocationIndexTree) hopper.getLocationIndex()); + // TODO: allow tests of non-CH? + AlgorithmOptions algoOpts = AlgorithmOptions.start() + .maxVisitedNodes((int) 1e20) + .build(); + MapMatching mapMatching = new MapMatching(hopper, algoOpts); + + // start tests: + StopWatch sw = new StopWatch().start(); + try { + printLocationIndexMatchQuery(locationIndex); + printTimeOfMapMatchQuery(hopper, mapMatching); + System.gc(); + logger.info("store into " + propLocation); + } catch (Exception ex) { + logger.error("Problem while measuring " + graphLocation, ex); + put("error", ex.toString()); + } finally { + put("measurement.count", count); + put("measurement.seed", seed); + put("measurement.time", sw.stop().getTime()); + System.gc(); + put("measurement.totalMB", Helper.getTotalMB()); + put("measurement.usedMB", Helper.getUsedMB()); + try { + store(new FileWriter(propLocation)); + } catch (IOException ex) { + logger.error( + "Problem while storing properties " + graphLocation + ", " + propLocation, + ex); + } + } + } + + /** + * Test the performance of finding candidate points for the index (which is run for every GPX + * entry). + * + */ + private void printLocationIndexMatchQuery(final LocationIndexMatch idx) { + final double latDelta = bbox.maxLat - bbox.minLat; + final double lonDelta = bbox.maxLon - bbox.minLon; + final Random rand = new Random(seed); + MiniPerfTest miniPerf = new MiniPerfTest() { + @Override + public int doCalc(boolean warmup, int run) { + double lat = rand.nextDouble() * latDelta + bbox.minLat; + double lon = rand.nextDouble() * lonDelta + bbox.minLon; + int val = idx.findNClosest(lat, lon, EdgeFilter.ALL_EDGES, rand.nextDouble() * 500) + .size(); + return val; + } + }.setIterations(count).start(); + print("location_index_match", miniPerf); + } + + /** + * Test the time taken for map matching on random routes. Note that this includes the index + * lookups (previous tests), so will be affected by those. Otherwise this is largely testing the + * routing and HMM performance. + */ + private void printTimeOfMapMatchQuery(final GraphHopper hopper, final MapMatching mapMatching) { + + // pick random start/end points to create a route, then pick random points from the route, + // and then run the random points through map-matching. + final double latDelta = bbox.maxLat - bbox.minLat; + final double lonDelta = bbox.maxLon - bbox.minLon; + final Random rand = new Random(seed); + // this takes a while, so we'll limit it to 100 tests: + int n = count; + if (n > 100) { + logger.warn("map matching query tests take a while, so we'll only do 100 iterations (instead of " + count + ")"); + n = 100; + } + MiniPerfTest miniPerf = new MiniPerfTest() { + @Override + public int doCalc(boolean warmup, int run) { + boolean foundPath = false; + + // keep going until we find a path (which we may not for certain start/end points) + while (!foundPath) { + + // create random points and find route between: + double lat0 = bbox.minLat + rand.nextDouble() * latDelta; + double lon0 = bbox.minLon + rand.nextDouble() * lonDelta; + double lat1 = bbox.minLat + rand.nextDouble() * latDelta; + double lon1 = bbox.minLon + rand.nextDouble() * lonDelta; + GHResponse r = hopper.route(new GHRequest(lat0, lon0, lat1, lon1)); + + // if found, use it for map mathching: + if (!r.hasErrors()) { + foundPath = true; + long time = 0; + double sampleProportion = rand.nextDouble(); + GHPoint prev = null; + List mock = new ArrayList(); + PointList points = r.getBest().getPoints(); + // loop through points and add (approximately) sampleProportion of them: + for (GHPoint p : points) { + if (null != prev && rand.nextDouble() < sampleProportion) { + // estimate a reasonable time taken since the last point, so we + // can give the GPXEntry a time. Use the distance between the + // points and a random speed to estimate a time. + double dx = distCalc.calcDist(prev.lat, prev.lon, p.lat, p.lon); + double speedKPH = rand.nextDouble() * 100; + double dt = (dx / 1000) / speedKPH * 3600000; + time += (long) dt; + // randomise the point lat/lon (i.e. so it's not + // exactly on the route): + GHPoint randomised = distCalc.projectCoordinate(p.lat, p.lon, + 20 * rand.nextDouble(), 360 * rand.nextDouble()); + mock.add(new GPXEntry(randomised, time)); + } + prev = p; + } + // now match, provided there are enough points + if (mock.size() > 2) { + MatchResult match = mapMatching.doWork(mock); + // return something non-trivial, to avoid JVM optimizing away + return match.getEdgeMatches().size(); + } else { + foundPath = false; // retry + } + } + } + return 0; + } + }.setIterations(n).start(); + print("map_match", miniPerf); + } + + void print(String prefix, MiniPerfTest perf) { + logger.info(prefix + ": " + perf.getReport()); + put(prefix + ".sum", perf.getSum()); + put(prefix + ".min", perf.getMin()); + put(prefix + ".mean", perf.getMean()); + put(prefix + ".max", perf.getMax()); + } + + void put(String key, Object val) { + // convert object to string to make serialization possible + properties.put(key, "" + val); + } + + private void store(FileWriter fileWriter) throws IOException { + for (Entry e : properties.entrySet()) { + fileWriter.append(e.getKey()); + fileWriter.append("="); + fileWriter.append(e.getValue()); + fileWriter.append("\n"); + } + fileWriter.flush(); + } +}