Skip to content
This repository has been archived by the owner on Mar 1, 2021. It is now read-only.

WIP: handling viterbi breaks as multiple sequences #87

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2c1a010
initial work for handling viterbi breaks as multiple sequences
kodonnell Dec 11, 2016
d979ffd
handle case where viterbi breaks immediately after initialization
kodonnell Dec 12, 2016
6b856f1
merge U-turn work
kodonnell Dec 30, 2016
ade4037
refactoring sequences
kodonnell Jan 13, 2017
4b24cbc
use MatchEntry internally instead of GPXEntry
kodonnell Jan 14, 2017
5832623
debug and fix tests
kodonnell Jan 15, 2017
b6c3af4
rename timestep -> viterbimatchentry
kodonnell Jan 15, 2017
c0e5574
fix other tests
kodonnell Jan 15, 2017
2d184a2
tidying calcpath and gpxfile/main
kodonnell Jan 15, 2017
791e53c
web stuff ...
kodonnell Jan 15, 2017
eed78bf
giving up on that test ...
kodonnell Jan 15, 2017
ac105e7
woops, don't need that anymore ...
kodonnell Jan 15, 2017
d6bf213
refactor + tidy + all tests passing
kodonnell Jan 31, 2017
4e217d0
contiguous sequences
kodonnell Jan 31, 2017
f629883
undo test change to fix test change
kodonnell Feb 1, 2017
9e6cc60
add logging in again as per @stefanholder's request
kodonnell Feb 6, 2017
9d6f84b
Merge branch 'master' into sequences
kodonnell Feb 26, 2017
7f45557
note funny bug ...
kodonnell Feb 26, 2017
4a7420a
some changes as per @stefanholder
kodonnell Mar 20, 2017
13707e1
bringing back the missing readme
kodonnell Mar 20, 2017
efb7b57
a few more tidyups
kodonnell Mar 20, 2017
956e7d0
more tidy-ups
kodonnell Mar 20, 2017
1dd88e8
utilise LocationIndexTree.findWithinRadius
kodonnell Mar 20, 2017
56df0ab
ugly hacky gui ...
kodonnell Mar 28, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* rory, support milisecond gpx timestamps, see #4
* stefanholder, Stefan Holder, BMW AG, creating and integrating the hmm-lib (#49, #66, #69) and
penalizing inner-link U-turns (#88, #91)
* kodonnell, adding support for CH and other algorithms (#60) and penalizing inner-link U-turns (#88)
* kodonnell, adding support for CH and other algorithms (#60) and penalizing inner-link U-turns
(#88) and handling sequence breaks as separate sequences (#87).

For GraphHopper contributors see [here](https://github.com/graphhopper/graphhopper/blob/master/CONTRIBUTORS.md).
145 changes: 145 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
## Map Matching based on GraphHopper

[![Build Status](https://secure.travis-ci.org/graphhopper/map-matching.png?branch=master)](http://travis-ci.org/graphhopper/map-matching)

This repository will soon move directly to [graphhopper/graphhopper](https://github.com/graphhopper/graphhopper). Give us your star there too!

Map matching is the process to match a sequence of real world coordinates into a digital map.
Read more at [Wikipedia](https://en.wikipedia.org/wiki/Map_matching). It can be used for tracking vehicles' GPS information, important for further digital analysis. Or e.g. attaching turn instructions for any recorded GPX route.

Currently this project is under heavy development but produces already good results for various use cases. Let us know if not and create an issue!

See the demo in action (black is GPS track, green is matched result):

![map-matching-example](https://cloud.githubusercontent.com/assets/129644/14740686/188a181e-0891-11e6-820c-3bd0a975f8a5.png)

### License

Apache License 2.0

### Discussion

Discussion happens [here](https://discuss.graphhopper.com/c/graphhopper/map-matching).

### Installation and Usage

Java 8 and Maven >=3.3 are required. For the 'core' module Java 7 is sufficient.

Then you need to import the area you want to do map-matching on:

```bash
git checkout [stable-branch] # optional
./map-matching.sh action=import datasource=./some-dir/osm-file.pbf vehicle=car
```

As an example you use `datasource=./map-data/leipzig_germany.osm.pbf` as road network base or any other pbf or xml from [here](http://download.geofabrik.de/).

The optional parameter `vehicle` defines the routing profile like `car`, `bike`, `motorcycle` or `foot`.
You can also provide a comma separated list. For all supported values see the variables in the [FlagEncoderFactory](https://github.com/graphhopper/graphhopper/blob/0.7/core/src/main/java/com/graphhopper/routing/util/FlagEncoderFactory.java) of GraphHopper.

If you have already imported a datasource with a specific profile, you need to remove the folder graph-cache in your map-matching root directory.

Now you can do these matches:
```bash
./map-matching.sh action=match gpx=./some-dir/*.gpx
```

As example use `gpx=./matching-core/src/test/resources/*.gpx` or one specific gpx file.

Possible arguments are:
```bash
instructions=de # default=, type=String, if an country-iso-code (like en or de) is specified turn instructions are included in the output, leave empty or default to avoid this
gps_accuracy=15 # default=15, type=int, unit=meter, the precision of the used device
```

This will produce gpx results similar named as the input files.

Developer note: After changing the code you should run `mvn clean` before running `map-matching.sh`
again.

### UI and matching Service

Start via:
```bash
./map-matching.sh action=start-server
```

Access the simple UI via localhost:8989.

You can post GPX files and get back snapped results as GPX or as compatible GraphHopper JSON. An example curl request is:
```bash
curl -XPOST -H "Content-Type: application/gpx+xml" -d @/path/to/gpx/file.gpx "localhost:8989/match?vehicle=car&type=json"
```

#### Development tools

Determine the maximum bounds of one or more GPX file:
```bash
./map-matching.sh action=getbounds gpx=./track-data/.*gpx
```

#### Java usage

Or use this Java snippet:

```java
// import OpenStreetMap data
GraphHopper hopper = new GraphHopperOSM();
hopper.setDataReaderFile("./map-data/leipzig_germany.osm.pbf");
hopper.setGraphHopperLocation("./target/mapmatchingtest");
CarFlagEncoder encoder = new CarFlagEncoder();
hopper.setEncodingManager(new EncodingManager(encoder));
hopper.getCHFactoryDecorator().setEnabled(false);
hopper.importOrLoad();

// create MapMatching object, can and should be shared accross threads
String algorithm = Parameters.Algorithms.DIJKSTRA_BI;
Weighting weighting = new FastestWeighting(encoder);
AlgorithmOptions algoOptions = new AlgorithmOptions(algorithm, weighting);
MapMatching mapMatching = new MapMatching(hopper, algoOptions);

// do the actual matching, get the GPX entries from a file or via stream
List<GPXEntry> inputGPXEntries = new GPXFile().doImport("nice.gpx").getEntries();
MatchResult mr = mapMatching.doWork(inputGPXEntries);

// return GraphHopper edges with all associated GPX entries
List<EdgeMatch> matches = mr.getEdgeMatches();
// now do something with the edges like storing the edgeIds or doing fetchWayGeometry etc
matches.get(0).getEdgeState();
```

with this maven dependency:

```xml
<dependency>
<groupId>com.graphhopper</groupId>
<artifactId>map-matching</artifactId>
<!-- or 0.9-SNAPSHOT for the unstable -->
<version>0.8.2</version>
</dependency>
```

### Note

Note that the edge and node IDs from GraphHopper will change for different PBF files,
like when updating the OSM data.

### About

The map matching algorithm mainly follows the approach described in

*Newson, Paul, and John Krumm. "Hidden Markov map matching through noise and sparseness."
Proceedings of the 17th ACM SIGSPATIAL International Conference on Advances in Geographic
Information Systems. ACM, 2009.*

This algorithm works as follows. For each input GPS position, a number of
map matching candidates within a certain radius around the GPS position is computed.
The [Viterbi algorithm](https://en.wikipedia.org/wiki/Viterbi_algorithm) as provided by the
[hmm-lib](https://github.com/bmwcarit/hmm-lib) is then used to compute the most likely sequence
of map matching candidates. Thereby, the distances between GPS positions and map matching
candidates as well as the routing distances between consecutive map matching candidates are taken
into account. The GraphHopper routing engine is used to find candidates and to compute routing
distances.

Before GraphHopper 0.8, [this faster but more heuristic approach](https://karussell.wordpress.com/2014/07/28/digitalizing-gpx-points-or-how-to-track-vehicles-with-graphhopper/)
was used.
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,46 @@
import com.graphhopper.util.GPXEntry;

/**
* During map matching this represents a map matching candidate, i.e. a potential snapped
* point of a GPX entry. After map matching, this represents the map matched point of
* an GPX entry.
* During map matching this represents a map matching candidate, i.e. a potential snapped point of a
* GPX entry. After map matching, this represents the map matched point of an GPX entry.
* <p>
* A GPXEntry can either be at an undirected real (tower) node or at a directed virtual node.
* If this is at a directed virtual node then incoming paths from any previous GPXExtension
* should arrive through {@link #getIncomingVirtualEdge()} and outgoing paths to any following
* GPXExtension should start with {@link #getOutgoingVirtualEdge()}. This is achieved by
* penalizing other edges for routing. Note that virtual nodes are always connected to their
* adjacent nodes via 2 virtual edges (not counting reverse virtual edges).
* A GPXEntry can either be at an undirected real (tower) node or at a directed virtual node. If
* this is at a directed virtual node then incoming paths from any previous GPXExtension should
* arrive through {@link #getIncomingVirtualEdge()} and outgoing paths to any following GPXExtension
* should start with {@link #getOutgoingVirtualEdge()}. This is achieved by penalizing other edges
* for routing. Note that virtual nodes are always connected to their adjacent nodes via 2 virtual
* edges (not counting reverse virtual edges).
*
* @author Peter Karich
* @author kodonnell
* @author Stefan Holder
*/
public class GPXExtension {
public class Candidate {
/**
* The original GPX entry for which this candidate is for.
*/
private final GPXEntry entry;
/**
* The QueryResult defining this candidate location.
*/
private final QueryResult queryResult;
/**
* Flag for whether or not this is a directed candidate.
*/
private final boolean isDirected;
/**
* The virtual edge that should be used by incoming paths.
*/
private final EdgeIteratorState incomingVirtualEdge;
/**
* The virtual edge that should be used by outgoing paths.
*/
private final EdgeIteratorState outgoingVirtualEdge;

/**
* Creates an undirected candidate for a real node.
*/
public GPXExtension(GPXEntry entry, QueryResult queryResult) {
public Candidate(GPXEntry entry, QueryResult queryResult) {
this.entry = entry;
this.queryResult = queryResult;
this.isDirected = false;
Expand All @@ -59,9 +73,9 @@ public GPXExtension(GPXEntry entry, QueryResult queryResult) {
/**
* Creates a directed candidate for a virtual node.
*/
public GPXExtension(GPXEntry entry, QueryResult queryResult,
VirtualEdgeIteratorState incomingVirtualEdge,
VirtualEdgeIteratorState outgoingVirtualEdge) {
public Candidate(GPXEntry entry, QueryResult queryResult,
VirtualEdgeIteratorState incomingVirtualEdge,
VirtualEdgeIteratorState outgoingVirtualEdge) {
this.entry = entry;
this.queryResult = queryResult;
this.isDirected = true;
Expand All @@ -78,8 +92,8 @@ public QueryResult getQueryResult() {
}

/**
* Returns whether this GPXExtension is directed. This is true if the snapped point
* is a virtual node, otherwise the snapped node is a real (tower) node and false is returned.
* Returns whether this GPXExtension is directed. This is true if the snapped point is a virtual
* node, otherwise the snapped node is a real (tower) node and false is returned.
*/
public boolean isDirected() {
return isDirected;
Expand Down Expand Up @@ -113,12 +127,9 @@ public EdgeIteratorState getOutgoingVirtualEdge() {

@Override
public String toString() {
return "GPXExtension{" +
"closest node=" + queryResult.getClosestNode() +
" at " + queryResult.getSnappedPoint().getLat() + "," +
queryResult.getSnappedPoint().getLon() +
", incomingEdge=" + incomingVirtualEdge +
", outgoingEdge=" + outgoingVirtualEdge +
'}';
return "GPXExtension{" + "closest node=" + queryResult.getClosestNode() + " at "
+ queryResult.getSnappedPoint().getLat() + ","
+ queryResult.getSnappedPoint().getLon() + ", incomingEdge=" + incomingVirtualEdge
+ ", outgoingEdge=" + outgoingVirtualEdge + '}';
}
}

This file was deleted.

25 changes: 14 additions & 11 deletions matching-core/src/main/java/com/graphhopper/matching/GPXFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public class GPXFile {
static final String DATE_FORMAT_Z_MS = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
private final List<GPXEntry> entries;
private boolean includeElevation = false;
private InstructionList instructions;
private List<InstructionList> instructions;

public GPXFile() {
entries = new ArrayList<GPXEntry>();
Expand All @@ -61,14 +61,14 @@ public GPXFile(List<GPXEntry> entries) {
this.entries = entries;
}

public GPXFile(MatchResult mr, InstructionList il) {
public GPXFile(MatchResult mr, List<InstructionList> il) {
this.instructions = il;
this.entries = new ArrayList<GPXEntry>(mr.getEdgeMatches().size());
// TODO fetch time from GPX or from calculated route?
long time = 0;
for (int emIndex = 0; emIndex < mr.getEdgeMatches().size(); emIndex++) {
EdgeMatch em = mr.getEdgeMatches().get(emIndex);
PointList pl = em.getEdgeState().fetchWayGeometry(emIndex == 0 ? 3 : 2);
MatchedEdge em = mr.getEdgeMatches().get(emIndex);
PointList pl = em.edge.fetchWayGeometry(emIndex == 0 ? 3 : 2);
if (pl.is3D()) {
includeElevation = true;
}
Expand Down Expand Up @@ -198,17 +198,20 @@ public String createString() {
StringBuilder gpxOutput = new StringBuilder(header);
gpxOutput.append("\n<trk><name>").append("GraphHopper MapMatching").append("</name>");

// TODO: is this correct? how do we know there's a 'gap' in the instructions i.e. multiple
// sequences? Do instructions only make sense for a single sequence?
if (instructions != null && !instructions.isEmpty()) {
gpxOutput.append("\n<rte>");
Instruction nextInstr = null;
for (Instruction currInstr : instructions) {
if (null != nextInstr) {
instructions.createRteptBlock(gpxOutput, nextInstr, currInstr);
for (InstructionList instr: instructions) {
Instruction nextInstr = null;
for (Instruction currInstr : instr) {
if (null != nextInstr) {
instr.createRteptBlock(gpxOutput, nextInstr, currInstr);
}
nextInstr = currInstr;
}

nextInstr = currInstr;
instr.createRteptBlock(gpxOutput, nextInstr, null);
}
instructions.createRteptBlock(gpxOutput, nextInstr, null);
gpxOutput.append("\n</rte>");
}

Expand Down
Loading