/*
* 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;
import com.graphhopper.GHRequest;
import com.graphhopper.GHResponse;
import com.graphhopper.PathWrapper;
import com.graphhopper.reader.osm.GraphHopperOSM;
import com.graphhopper.routing.AlgorithmOptions;
import com.graphhopper.routing.Path;
import com.graphhopper.routing.util.*;
import com.graphhopper.storage.GraphHopperStorage;
import com.graphhopper.storage.NodeAccess;
import com.graphhopper.storage.index.LocationIndex;
import com.graphhopper.util.BreadthFirstSearch;
import com.graphhopper.util.EdgeExplorer;
import com.graphhopper.util.EdgeIteratorState;
import com.graphhopper.util.GPXEntry;
import com.graphhopper.util.Helper;
import com.graphhopper.util.InstructionList;
import com.graphhopper.util.PMap;
import com.graphhopper.util.Parameters;
import com.graphhopper.util.PathMerger;
import com.graphhopper.util.Translation;
import com.graphhopper.util.TranslationMap;
import com.graphhopper.util.shapes.GHPoint;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
/**
*
* @author Peter Karich
* @author kodonnell
*/
@RunWith(Parameterized.class)
public class MapMatchingTest {
public final static TranslationMap SINGLETON = new TranslationMap().doImport();
// non-CH / CH test parameters
private final String parameterName;
private final TestGraphHopper hopper;
private final AlgorithmOptions algoOptions;
@Parameterized.Parameters(name = "{0}")
public static Collection<Object[]> algoOptions() {
// create hopper instance with CH enabled
CarFlagEncoder encoder = new CarFlagEncoder();
TestGraphHopper hopper = new TestGraphHopper();
hopper.setDataReaderFile("../map-data/leipzig_germany.osm.pbf");
hopper.setGraphHopperLocation("../target/mapmatchingtest-ch");
hopper.setEncodingManager(new EncodingManager(encoder));
hopper.importOrLoad();
// force CH
AlgorithmOptions chOpts = AlgorithmOptions.start()
.maxVisitedNodes(1000)
.hints(new PMap().put(Parameters.CH.DISABLE, false))
.build();
// flexible should fall back to defaults
AlgorithmOptions flexibleOpts = AlgorithmOptions.start()
// TODO: fewer nodes than for CH are possible (short routes & different finish condition & higher degree graph)
// .maxVisitedNodes(20)
.build();
return Arrays.asList(new Object[][]{
{"non-CH", hopper, flexibleOpts},
{"CH", hopper, chOpts}
});
}
public MapMatchingTest(String parameterName, TestGraphHopper hopper,
AlgorithmOptions algoOption) {
this.parameterName = parameterName;
this.algoOptions = algoOption;
this.hopper = hopper;
}
/**
* TODO: split this test up into smaller units with better names?
*/
@Test
public void testDoWork() {
MapMatching mapMatching = new MapMatching(hopper, algoOptions);
List<GPXEntry> inputGPXEntries = createRandomGPXEntries(
new GHPoint(51.358735, 12.360574),
new GHPoint(51.358594, 12.360032));
MatchResult mr = mapMatching.doWork(inputGPXEntries);
// make sure no virtual edges are returned
int edgeCount = hopper.getGraphHopperStorage().getAllEdges().getMaxId();
for (EdgeMatch em : mr.getEdgeMatches()) {
assertTrue("result contains virtual edges:" + em.getEdgeState().toString(),
em.getEdgeState().getEdge() < edgeCount);
}
// create street names
assertEquals(Arrays.asList("Platnerstraße", "Platnerstraße", "Platnerstraße"),
fetchStreets(mr.getEdgeMatches()));
assertEquals(mr.getGpxEntriesLength(), mr.getMatchLength(), 1.5);
assertEquals(mr.getGpxEntriesMillis(), mr.getMatchMillis());
Path path = mapMatching.calcPath(mr);
PathWrapper matchGHRsp = new PathWrapper();
new PathMerger().doWork(matchGHRsp, Collections.singletonList(path), SINGLETON.get("en"));
InstructionList il = matchGHRsp.getInstructions();
assertEquals(il.toString(), 2, il.size());
assertEquals("Platnerstraße", il.get(0).getName());
inputGPXEntries = createRandomGPXEntries(
new GHPoint(51.33099, 12.380267),
new GHPoint(51.330689, 12.380776));
mr = mapMatching.doWork(inputGPXEntries);
assertEquals(Arrays.asList("Windmühlenstraße", "Windmühlenstraße", "Bayrischer Platz",
"Bayrischer Platz", "Bayrischer Platz"), fetchStreets(mr.getEdgeMatches()));
assertEquals(mr.getGpxEntriesLength(), mr.getMatchLength(), .1);
assertEquals(mr.getGpxEntriesMillis(), mr.getMatchMillis(), 1);
path = mapMatching.calcPath(mr);
matchGHRsp = new PathWrapper();
new PathMerger().doWork(matchGHRsp, Collections.singletonList(path), SINGLETON.get("en"));
il = matchGHRsp.getInstructions();
assertEquals(il.toString(), 3, il.size());
assertEquals("Windmühlenstraße", il.get(0).getName());
assertEquals("Bayrischer Platz", il.get(1).getName());
// full path
inputGPXEntries = createRandomGPXEntries(
new GHPoint(51.377781, 12.338333),
new GHPoint(51.323317, 12.387085));
mapMatching = new MapMatching(hopper, algoOptions);
mapMatching.setMeasurementErrorSigma(20);
mr = mapMatching.doWork(inputGPXEntries);
assertEquals(mr.getGpxEntriesLength(), mr.getMatchLength(), 0.5);
assertEquals(mr.getGpxEntriesMillis(), mr.getMatchMillis(), 200);
assertEquals(138, mr.getEdgeMatches().size());
// TODO full path with 20m distortion
// TODO full path with 40m distortion
}
/**
* This test is to check behavior over large separated routes: it should
* work if the user sets the maxVisitedNodes large enough. Input path:
* https://graphhopper.com/maps/?point=51.23%2C12.18&point=51.45%2C12.59&layer=Lyrk
*/
@Test
public void testDistantPoints() {
// OK with 1000 visited nodes:
MapMatching mapMatching = new MapMatching(hopper, algoOptions);
List<GPXEntry> inputGPXEntries = createRandomGPXEntries(
new GHPoint(51.23, 12.18),
new GHPoint(51.45, 12.59));
MatchResult mr = mapMatching.doWork(inputGPXEntries);
assertEquals(57650, mr.getMatchLength(), 1);
assertEquals(2747796, mr.getMatchMillis(), 1);
// not OK when we only allow a small number of visited nodes:
AlgorithmOptions opts = AlgorithmOptions.start(algoOptions).maxVisitedNodes(1).build();
mapMatching = new MapMatching(hopper, opts);
try {
mr = mapMatching.doWork(inputGPXEntries);
fail("Expected sequence to be broken due to maxVisitedNodes being too small");
} catch (RuntimeException e) {
assertTrue(e.getMessage().startsWith("Sequence is broken for submitted track"));
}
}
/**
* This test is to check what happens when two GPX entries are on one edge
* which is longer than 'separatedSearchDistance' - which is always 66m. GPX
* input:
* https://graphhopper.com/maps/?point=51.359723%2C12.360108&point=51.358748%2C12.358798&point=51.358001%2C12.357597&point=51.358709%2C12.356511&layer=Lyrk
*/
@Test
public void testSmallSeparatedSearchDistance() {
List<GPXEntry> inputGPXEntries = new GPXFile()
.doImport("./src/test/resources/tour3-with-long-edge.gpx").getEntries();
MapMatching mapMatching = new MapMatching(hopper, algoOptions);
mapMatching.setMeasurementErrorSigma(20);
MatchResult mr = mapMatching.doWork(inputGPXEntries);
assertEquals(Arrays.asList("Weinligstraße", "Weinligstraße", "Weinligstraße",
"Fechnerstraße", "Fechnerstraße"), fetchStreets(mr.getEdgeMatches()));
assertEquals(mr.getGpxEntriesLength(), mr.getMatchLength(), 11); // TODO: this should be around 300m according to Google ... need to check
assertEquals(mr.getGpxEntriesMillis(), mr.getMatchMillis(), 3000);
}
/**
* This test is to check that loops are maintained. GPX input:
* https://graphhopper.com/maps/?point=51.343657%2C12.360708&point=51.344982%2C12.364066&point=51.344841%2C12.361223&point=51.342781%2C12.361867&layer=Lyrk
*/
@Test
public void testLoop() {
MapMatching mapMatching = new MapMatching(hopper, algoOptions);
// Need to reduce GPS accuracy because too many GPX are filtered out otherwise.
mapMatching.setMeasurementErrorSigma(40);
List<GPXEntry> inputGPXEntries = new GPXFile()
.doImport("./src/test/resources/tour2-with-loop.gpx").getEntries();
MatchResult mr = mapMatching.doWork(inputGPXEntries);
assertEquals(
Arrays.asList("Gustav-Adolf-Straße", "Gustav-Adolf-Straße", "Gustav-Adolf-Straße",
"Leibnizstraße", "Hinrichsenstraße", "Hinrichsenstraße",
"Tschaikowskistraße", "Tschaikowskistraße"),
fetchStreets(mr.getEdgeMatches()));
assertEquals(mr.getGpxEntriesLength(), mr.getMatchLength(), 5);
// TODO why is there such a big difference for millis?
assertEquals(mr.getGpxEntriesMillis(), mr.getMatchMillis(), 6000);
}
/**
* This test is to check that loops are maintained. GPX input:
* https://graphhopper.com/maps/?point=51.342439%2C12.361615&point=51.343719%2C12.362784&point=51.343933%2C12.361781&point=51.342325%2C12.362607&layer=Lyrk
*/
@Test
public void testLoop2() {
MapMatching mapMatching = new MapMatching(hopper, algoOptions);
// TODO smaller sigma like 40m leads to U-turn at Tschaikowskistraße
mapMatching.setMeasurementErrorSigma(50);
List<GPXEntry> inputGPXEntries = new GPXFile()
.doImport("./src/test/resources/tour-with-loop.gpx").getEntries();
MatchResult mr = mapMatching.doWork(inputGPXEntries);
assertEquals(Arrays.asList("Jahnallee, B 87, B 181", "Jahnallee, B 87, B 181",
"Jahnallee, B 87, B 181", "Jahnallee, B 87, B 181", "Funkenburgstraße",
"Gustav-Adolf-Straße", "Tschaikowskistraße", "Jahnallee, B 87, B 181",
"Lessingstraße", "Lessingstraße"), fetchStreets(mr.getEdgeMatches()));
}
/**
* This test is to check that U-turns are avoided when it's just measurement
* error, though do occur when a point goes up a road further than the
* measurement error. GPX input:
* https://graphhopper.com/maps/?point=51.343618%2C12.360772&point=51.34401%2C12.361776&point=51.343977%2C12.362886&point=51.344734%2C12.36236&point=51.345233%2C12.362055&layer=Lyrk
*/
@Test
public void testUTurns() {
// This test requires changing the default heading penalty, which does not work for CH.
if (parameterName.equals("CH")) {
return;
}
final AlgorithmOptions algoOptions = AlgorithmOptions.start()
// Reduce penalty to allow U-turns
.hints(new PMap().put(Parameters.Routing.HEADING_PENALTY, 50))
.build();
MapMatching mapMatching = new MapMatching(hopper, algoOptions);
List<GPXEntry> inputGPXEntries = new GPXFile()
.doImport("./src/test/resources/tour4-with-uturn.gpx").getEntries();
// with large measurement error, we expect no U-turn
mapMatching.setMeasurementErrorSigma(50);
MatchResult mr = mapMatching.doWork(inputGPXEntries);
assertEquals(Arrays.asList("Gustav-Adolf-Straße", "Gustav-Adolf-Straße", "Funkenburgstraße",
"Funkenburgstraße"), fetchStreets(mr.getEdgeMatches()));
// with small measurement error, we expect the U-turn
mapMatching.setMeasurementErrorSigma(10);
mr = mapMatching.doWork(inputGPXEntries);
assertEquals(
Arrays.asList("Gustav-Adolf-Straße", "Gustav-Adolf-Straße", "Funkenburgstraße",
"Funkenburgstraße", "Funkenburgstraße", "Funkenburgstraße"),
fetchStreets(mr.getEdgeMatches()));
}
static List<String> fetchStreets(List<EdgeMatch> emList) {
List<String> list = new ArrayList<String>();
int prevNode = -1;
List<String> errors = new ArrayList<String>();
for (EdgeMatch em : emList) {
String str = em.getEdgeState().getName();// + ":" + em.getEdgeState().getBaseNode() +
// "->" + em.getEdgeState().getAdjNode();
list.add(str);
if (prevNode >= 0) {
if (em.getEdgeState().getBaseNode() != prevNode) {
errors.add(str);
}
}
prevNode = em.getEdgeState().getAdjNode();
}
if (!errors.isEmpty()) {
throw new IllegalStateException("Errors:" + errors);
}
return list;
}
private List<GPXEntry> createRandomGPXEntries(GHPoint start, GHPoint end) {
hopper.route(new GHRequest(start, end).setWeighting("fastest"));
return hopper.getEdges(0);
}
private void printOverview(GraphHopperStorage graph, LocationIndex locationIndex,
final double lat, final double lon, final double length) {
final NodeAccess na = graph.getNodeAccess();
int node = locationIndex.findClosest(lat, lon, EdgeFilter.ALL_EDGES).getClosestNode();
final EdgeExplorer explorer = graph.createEdgeExplorer();
new BreadthFirstSearch() {
double currDist = 0;
@Override
protected boolean goFurther(int nodeId) {
double currLat = na.getLat(nodeId);
double currLon = na.getLon(nodeId);
currDist = Helper.DIST_PLANE.calcDist(currLat, currLon, lat, lon);
return currDist < length;
}
@Override
protected boolean checkAdjacent(EdgeIteratorState edge) {
System.out.println(edge.getBaseNode() + "->" + edge.getAdjNode() + " ("
+ Math.round(edge.getDistance()) + "): " + edge.getName() + "\t\t , distTo:"
+ currDist);
return true;
}
}.start(explorer, node);
}
// use a workaround to get access to paths
static class TestGraphHopper extends GraphHopperOSM {
TestGraphHopper() {
super();
getCHFactoryDecorator().setDisablingAllowed(true);
}
private List<Path> paths;
List<GPXEntry> getEdges(int index) {
Path path = paths.get(index);
Translation tr = getTranslationMap().get("en");
InstructionList instr = path.calcInstructions(tr);
return instr.createGPXList();
}
@Override
public List<Path> calcPaths(GHRequest request, GHResponse rsp) {
paths = super.calcPaths(request, rsp);
return paths;
}
}
}