From dd04fd80fd0c5d358364b78ca18728d7757c063f Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 1 Jan 2017 13:19:32 +0100 Subject: [PATCH 1/5] Added main repo link --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 444ae716..b26b4f97 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![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. From 746b1e754693b7a5e8025f21d6c7e9e9644e780f Mon Sep 17 00:00:00 2001 From: Stefan Holder Date: Mon, 9 Jan 2017 17:13:23 +0100 Subject: [PATCH 2/5] Update README.md (matching algo + mvn) (#91) --- README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 444ae716..91914e13 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,9 @@ gps_accuracy=15 # default=15, type=int, unit=meter, the precision o 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: @@ -122,4 +125,20 @@ like when updating the OSM data. ### About -See [this project](https://github.com/bmwcarit/hmm-lib) from [Stefan](https://github.com/stefanholder) which is used in combination with the GraphHopper routing engine and is used as the algorithmic approach now. Before it was [this faster but more heuristic approach](https://karussell.wordpress.com/2014/07/28/digitalizing-gpx-points-or-how-to-track-vehicles-with-graphhopper/). +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. \ No newline at end of file From 10117c769b0c329708071a6c440005ee1669d6ad Mon Sep 17 00:00:00 2001 From: Stefan Holder Date: Mon, 9 Jan 2017 17:23:53 +0100 Subject: [PATCH 3/5] Dedupe QueryResults by closest node (#91) --- CONTRIBUTORS.md | 4 +- .../matching/LocationIndexMatch.java | 14 +++-- .../com/graphhopper/matching/MapMatching.java | 54 ++++++++++++++----- .../graphhopper/matching/MapMatchingMain.java | 2 + 4 files changed, 55 insertions(+), 19 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 84980bf5..82a29074 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -5,7 +5,7 @@ * michaz, very important hidden markov improvement via hmm-lib, see #49 * 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 (#70) - * kodonnell, adding support for CH and other algorithms (#60) and penalizing inner-link U-turns (#70) + penalizing inner-link U-turns (#88, #91) + * kodonnell, adding support for CH and other algorithms (#60) and penalizing inner-link U-turns (#88) For GraphHopper contributors see [here](https://github.com/graphhopper/graphhopper/blob/master/CONTRIBUTORS.md). diff --git a/matching-core/src/main/java/com/graphhopper/matching/LocationIndexMatch.java b/matching-core/src/main/java/com/graphhopper/matching/LocationIndexMatch.java index 7e45da36..df35caa2 100644 --- a/matching-core/src/main/java/com/graphhopper/matching/LocationIndexMatch.java +++ b/matching-core/src/main/java/com/graphhopper/matching/LocationIndexMatch.java @@ -52,16 +52,24 @@ public LocationIndexMatch(GraphHopperStorage graph, LocationIndexTree index) { this.index = index; } - public List findNClosest(final double queryLat, final double queryLon, final EdgeFilter edgeFilter, - double gpxAccuracyInMetern) { + /** + * Returns all edges that are within the specified radius around the queried position. + * Searches at most 9 cells to avoid performance problems. Hence, if the radius is larger than + * the cell width then not all edges might be returned. + * + * @param radius in meters + */ + public List findNClosest(final double queryLat, final double queryLon, + final EdgeFilter edgeFilter, double radius) { // Return ALL results which are very close and e.g. within the GPS signal accuracy. // Also important to get all edges if GPS point is close to a junction. - final double returnAllResultsWithin = distCalc.calcNormalizedDist(gpxAccuracyInMetern); + final double returnAllResultsWithin = distCalc.calcNormalizedDist(radius); // implement a cheap priority queue via List, sublist and Collections.sort final List queryResults = new ArrayList(); GHIntHashSet set = new GHIntHashSet(); + // Doing 2 iterations means searching 9 tiles. for (int iteration = 0; iteration < 2; iteration++) { // should we use the return value of earlyFinish? index.findNetworkEntries(queryLat, queryLon, set, iteration); diff --git a/matching-core/src/main/java/com/graphhopper/matching/MapMatching.java b/matching-core/src/main/java/com/graphhopper/matching/MapMatching.java index f2b53a2f..7c4c8896 100644 --- a/matching-core/src/main/java/com/graphhopper/matching/MapMatching.java +++ b/matching-core/src/main/java/com/graphhopper/matching/MapMatching.java @@ -183,18 +183,29 @@ public MatchResult doWork(List gpxList) { // now find each of the entries in the graph: final EdgeFilter edgeFilter = new DefaultEdgeFilter(algoOptions.getWeighting().getFlagEncoder()); - List> queriesPerEntry = lookupGPXEntries(filteredGPXEntries, edgeFilter); + List> queriesPerEntry = + lookupGPXEntries(filteredGPXEntries, edgeFilter); - // now look up the entries up in the graph: + // Add virtual nodes and edges to the graph so that candidates on edges can be represented + // by virtual nodes. final QueryGraph queryGraph = new QueryGraph(routingGraph).setUseEdgeExplorerCache(true); List allQueryResults = new ArrayList<>(); - for (List qrs: queriesPerEntry) + for (Collection qrs: queriesPerEntry) allQueryResults.addAll(qrs); queryGraph.lookup(allQueryResults); + // Different QueryResults can have the same tower node as their closest node. + // Hence, we now dedupe the query results of each GPX entry by their closest node (#91). + // This must be done after calling queryGraph.lookup() since this replaces some of the + // QueryResult nodes with virtual nodes. Virtual nodes are not deduped since there is at + // most one QueryResult per edge and virtual nodes are inserted into the middle of an edge. + // Reducing the number of QueryResults improves performance since less shortest/fastest + // routes need to be computed. + queriesPerEntry = dedupeQueryResultsByClosestNode(queriesPerEntry); + logger.debug("================= Query results ================="); int i = 1; - for (List entries : queriesPerEntry) { + for (Collection entries : queriesPerEntry) { logger.debug("Query results for GPX entry {}", i++); for (QueryResult qr : entries) { logger.debug("Node id: {}, virtual: {}, snapped on: {}, pos: {},{}, " @@ -269,26 +280,41 @@ private List filterGPXEntries(List gpxList) { } /** - * Find the possible locations of each qpxEntry in the graph. + * Find the possible locations (edges) of each GPXEntry in the graph. */ - private List> lookupGPXEntries(List gpxList, + private List> lookupGPXEntries(List gpxList, EdgeFilter edgeFilter) { - List> gpxEntryLocations = new ArrayList<>(); + final List> gpxEntryLocations = new ArrayList<>(); for (GPXEntry gpxEntry : gpxList) { - gpxEntryLocations.add(locationIndex.findNClosest(gpxEntry.lat, gpxEntry.lon, edgeFilter, - measurementErrorSigma)); + final List queryResults = locationIndex.findNClosest( + gpxEntry.lat, gpxEntry.lon, edgeFilter, measurementErrorSigma); + gpxEntryLocations.add(queryResults); } return gpxEntryLocations; } + private List> dedupeQueryResultsByClosestNode( + List> queriesPerEntry) { + final List> result = new ArrayList<>(queriesPerEntry.size()); + + for (Collection queryResults : queriesPerEntry) { + final Map dedupedQueryResults = new HashMap<>(); + for (QueryResult qr : queryResults) { + dedupedQueryResults.put(qr.getClosestNode(), qr); + } + result.add(dedupedQueryResults.values()); + } + return result; + } + /** * Creates TimeSteps with candidates for the GPX entries but does not create emission or * transition probabilities. Creates directed candidates for virtual nodes and undirected * candidates for real nodes. */ private List> createTimeSteps( - List filteredGPXEntries, List> queriesPerEntry, + List filteredGPXEntries, List> queriesPerEntry, QueryGraph queryGraph) { final int n = filteredGPXEntries.size(); if (queriesPerEntry.size() != n) { @@ -300,7 +326,7 @@ private List> createTimeSteps( for (int i = 0; i < n; i++) { GPXEntry gpxEntry = filteredGPXEntries.get(i); - List queryResults = queriesPerEntry.get(i); + final Collection queryResults = queriesPerEntry.get(i); List candidates = new ArrayList<>(); for (QueryResult qr: queryResults) { @@ -512,7 +538,7 @@ private double penalizedPathDistance(Path path, private MatchResult computeMatchResult(List> seq, List gpxList, - List> queriesPerEntry, + List> queriesPerEntry, EdgeExplorer explorer) { final Map virtualEdgesMap = createVirtualEdgesMap( queriesPerEntry, explorer); @@ -611,10 +637,10 @@ private boolean isVirtualNode(int node) { * Returns a map where every virtual edge maps to its real edge with correct orientation. */ private Map createVirtualEdgesMap( - List> queriesPerEntry, EdgeExplorer explorer) { + List> queriesPerEntry, EdgeExplorer explorer) { // TODO For map key, use the traversal key instead of string! Map virtualEdgesMap = new HashMap<>(); - for (List queryResults: queriesPerEntry) { + for (Collection queryResults: queriesPerEntry) { for (QueryResult qr: queryResults) { if (isVirtualNode(qr.getClosestNode())) { EdgeIterator iter = explorer.setBaseNode(qr.getClosestNode()); diff --git a/matching-core/src/main/java/com/graphhopper/matching/MapMatchingMain.java b/matching-core/src/main/java/com/graphhopper/matching/MapMatchingMain.java index 49479090..ee890165 100644 --- a/matching-core/src/main/java/com/graphhopper/matching/MapMatchingMain.java +++ b/matching-core/src/main/java/com/graphhopper/matching/MapMatchingMain.java @@ -86,6 +86,8 @@ private void start(CmdArgs args) { algorithm(Parameters.Algorithms.DIJKSTRA_BI).traversalMode(hopper.getTraversalMode()). weighting(new FastestWeighting(firstEncoder)). maxVisitedNodes(args.getInt("max_visited_nodes", 1000)). + // Penalizing inner-link U-turns only works with fastest weighting, since + // shortest weighting does not apply penalties to unfavored virtual edges. hints(new HintsMap().put("weighting", "fastest").put("vehicle", firstEncoder.toString())). build(); MapMatching mapMatching = new MapMatching(hopper, opts); From 0726020237e27ed1a1ae95f5d2bce470dadaf67b Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 11 Feb 2017 15:03:42 +0100 Subject: [PATCH 4/5] Fix doc mistake --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e72c1f43..eb3cdb97 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Or use this Java snippet: ```java // import OpenStreetMap data -GraphHopper hopper = new GraphHopper(); +GraphHopper hopper = new GraphHopperOSM(); hopper.setOSMFile("./map-data/leipzig_germany.osm.pbf"); hopper.setGraphHopperLocation("./target/mapmatchingtest"); CarFlagEncoder encoder = new CarFlagEncoder(); @@ -143,4 +143,4 @@ into account. The GraphHopper routing engine is used to find candidates and to c 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. \ No newline at end of file +was used. From cfe744c69b3d7629e8dbc1cebaaa507cef270f33 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 16 Feb 2017 17:16:28 +0100 Subject: [PATCH 5/5] fixed java usage in documentation --- README.md | 146 ------------------------------------------------------ 1 file changed, 146 deletions(-) diff --git a/README.md b/README.md index eb3cdb97..e69de29b 100644 --- a/README.md +++ b/README.md @@ -1,146 +0,0 @@ -## 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.setOSMFile("./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 - -GraphHopperStorage graph = hopper.getGraphHopperStorage(); -LocationIndexMatch locationIndex = new LocationIndexMatch(graph, - (LocationIndexTree) hopper.getLocationIndex()); -MapMatching mapMatching = new MapMatching(graph, locationIndex, encoder); - -// do the actual matching, get the GPX entries from a file or via stream -List inputGPXEntries = new GPXFile().doImport("nice.gpx").getEntries(); -MatchResult mr = mapMatching.doWork(inputGPXEntries); - -// return GraphHopper edges with all associated GPX entries -List 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 - - com.graphhopper - map-matching - - 0.8.2 - -``` - -### 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.