UNPKG

contraction-hierarchy-js

Version:
197 lines (116 loc) 7.91 kB
# 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).