contraction-hierarchy-js
Version:
Contraction Hierarchy
197 lines (116 loc) • 7.91 kB
Markdown
# contraction-hierarchy-js
:scream: Scary-Fast Pathfinding for NodeJS using [Contraction Hierarchies](https://en.wikipedia.org/wiki/Contraction_hierarchies)
## When to Use a Contraction Hierarchy
The typical use case of using a Contraction hierarchy over Dijkstras Algorithm or a heuristic-based approach like A* is where you have static data that changes infrequently. A good example would be a road network (one that does not attempt to account for road closures and traffic). Being able to pre-process networks up front allows for a greatly increased speed of pathfinding at runtime.
## Changes in 1.0
- Ability to work with non-geojson data via a manual edge API.
- Ability to work with directed networks (using the manual API)
- Export data as GeoJSON, edge _id list, node list, or edge properties array.
## Install
```
npm install --save contraction-hierarchy-js
```
## Quickstart
```
const fs = require('fs');
const { Graph, CoordinateLookup } = require('../index.js');
const geofile = fs.readFileSync('../networks/basic.geojson', 'utf8');
const geojson = JSON.parse(geofile);
const graph = new Graph(geojson);
// build hierarchy. this step may take a while.
graph.contractGraph();
const finder = graph.createPathfinder({ ids: true, path: true, nodes: true, properties: true });
// create a coordinate lookup to be able to input arbitrary coordinate pairs
// and return the nearest coordinates in the network
const lookup = new CoordinateLookup(graph);
const coords1 = lookup.getClosestNetworkPt(-116.45, 41.96);
const coords2 = lookup.getClosestNetworkPt(-117.45, 40.96);
const path = finder.queryContractionHierarchy(coords1, coords2);
console.log(path);
```
## API
### Graph
```
const graph = new Graph(geojson, options);
```
Creates a new `Graph` object.
Both parameters; `geojson` and `options` are optional.
`geojson` is a GeoJSON linestring network; [example](https://raw.githubusercontent.com/royhobbstn/contraction-hierarchy-js/master/networks/basic.geojson).
- Your features` properties` object must contain a unique `_id` (number) and a `_cost` (number).
- GeoJSON networks are assumed to be undirected networks. If you need to construct a directed network, please use the manual API.
`options` is an object with one attribute:
- `debugMode` - defaults to false. Set it to true to see miscellaneous data validation and contraction progress messages.
### Graph Methods
```
graph.addEdge(start, end, edge_properties)
```
If your data is not GeoJSON, you can instead use the manual API.
- `start` and `end` are string values corresponding to Node names. `edge_properties` is an object listing the properties of the edge between the `start` and `end` nodes.
- `edge_properties` object must contain a unique `_id` (number) and a `_cost` (number).
```
graph.contractGraph()
```
The `contractGraph` method will build a contraction hierarchy from your input data. This step could take a while! For extremely large datasets (example: highly detailed road networks of large geographic areas) the build time could extend for hours, or not be feasible at all. I would highly recommend using `{ debugMode = true }` as your `options` parameter when initializing your `Graph`, to give you a gauge on the contraction progress of your network.
```
const finder = graph.createPathfinder(options);
```
The `createPathfinder` method creates a pathFinder object with which you can use to query your network. The main purpose is to be able to configure graph outputs with the `options` object.
By default, any queries you make on the network will return with `{ total_cost: (number) }` for your given path. To add additional properties, you can supply either/or/none of the following for the `options` object:
`{ids: true}`: Will return an ordered array of edge IDs corresponding to the `_id` attribute in the original geojson.
`{path: true}`: Will return a geojson linestring path with all original geojson attributes.
`{nodes: true}`: Will return an ordered array of nodes that the path follows.
`{properties: true}`: Will return an ordered array of properties of each edge.
### Load and Save
```
graph.saveCH()
```
Create a stringified serialized version of your contracted network. This is immensely useful to be able to re-use your contracted network, without having to incur the cost of contraction repeatedly.
```
const graph = graph.loadCH(network)
```
Load a stringified serialized contracted network (that was saved previously via the `saveCH` method).
```
graph.savePbfCH(filename);
```
Save network as a PBF file (much more compact!) NodeJS only.
```
graph.savePbfCH(filename);
```
Save network as a PBF file (much more compact!) NodeJS only.
```
graph.loadPbfCH(buffer);
```
Load a network that was saved to PBF. Can be used in the browser or NodeJS.
### Finder Methods
```
const path = finder.queryContractionHierarchy(start, end);
```
To query the graph, use the `queryContractionHierarchy` method. It expects `start` and `end` coordinates, where each is in the form: `[-110.45, 35.4]` ([lng, lat])
## Coordinate Lookup
```
const lookup = new CoordinateLookup(graph);
const coords1 = lookup.getClosestNetworkPt(-101.359, 43.341);
const coords2 = lookup.getClosestNetworkPt(-91.669, 40.195);
```
When using `queryContractionHierarchy`, your start and end points must correspond exactly with start/end points of lines in your graph. Because this can be difficult to arrange without a lot of manual work, I've built a helper to be able to find the closest coordinates in your graph to any arbitrary coordinate you supply.
## Performance
This is not benchmarking per se, as comparing a dijkstra implementation to a contraction hierarchy is not an apples to apples comparison. (Contraction hierarchies require a lengthy pre-processing step, whearas Dijkstras algorithm does not.)
Here is a comparison against a very fast implementation of Dijkstra via [Ngraph Path](https://github.com/anvaka/ngraph.path)
Dataset: USA major roads network (via freight analysis framework)
Nodes: 135308
Edges: 340981
`--max_old_space_size=7000` AWS t2.large (2vCPU, 8GB)
| | Contraction Time | 10,000 Random Routes | ms per route |
| --------------------------- | ---------------- | -------------------- | ------------- |
| * Dijkstra (via Ngraph) | 0 ms | 1232269 ms | 123.23 ms |
| * Contraction Hierarchy JS | 972786 ms | 3616 ms | 0.36 ms |
| ** Contraction Hierarchy JS | 972786 ms | 24013 ms | 2.40 ms |
* Basic (only distance calculated)
** Enriched (construct GeoJSON path)
As you can see, if your data is not highly dynamic, it makes sense to contract your network to get a tremendous runtime boost in speed.
I don't quite believe it myself, TBH, but there it is.
# Credits
Quite a few of the program internals were inspired from or directly ported from the excellent project NGraph. If you need a feature rich pathfinding solution and a contraction step is a dealbreaker, I highly recommend checking out [NGraph](https://github.com/anvaka/ngraph.path).
The coordinate lookup would not have been possible without the [geokdbush](https://github.com/mourner/geokdbush) library. [Mourner](https://github.com/mourner) is also the original creator of [TinyQueue](https://github.com/mourner/tinyqueue), a derivation of which is included in this program. Including this queue brought about some unbelievable performance improvements.
## Issues
Larger networks are problematic. Time to contract is obviously much higher. Memory issues start to become a factor as well. Become aquainted with the NodeJS command line argument: `--max_old_space_size=`. If you run into this, check out [this stackoverflow post](https://stackoverflow.com/questions/38558989/node-js-heap-out-of-memory).