/*
* 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<String, String> properties = new TreeMap<String, String>();
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 <measurement property>=<value>
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<GPXEntry> mock = new ArrayList<GPXEntry>();
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<String, String> e : properties.entrySet()) {
fileWriter.append(e.getKey());
fileWriter.append("=");
fileWriter.append(e.getValue());
fileWriter.append("\n");
}
fileWriter.flush();
}
}