├── .github └── workflows │ └── static.yml ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── main.js │ ├── search.js │ └── style.css ├── classes │ └── index.default.html ├── index.html ├── modules.html ├── modules │ ├── index.html │ └── types.html └── types │ ├── types.Coordinates.html │ ├── types.Edge.html │ ├── types.Key.html │ ├── types.PathFinderGraph.html │ ├── types.PathFinderOptions.html │ ├── types.Topology.html │ ├── types.Vertex.html │ └── types.Vertices.html ├── package-lock.json ├── package.json ├── src ├── compactor.ts ├── dijkstra.ts ├── index.ts ├── preprocessor.ts ├── round-coord.ts ├── topology.ts └── types.ts ├── test ├── 66.json ├── advent24.json ├── compactor.spec.js ├── large-network.json ├── network.json ├── osm-weight.js ├── path.spec.js ├── preprocessor.spec.js ├── topologySpec.js └── two-islands.json ├── tsconfig.cjs.json └── tsconfig.json /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["gh-pages"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | - name: Set up Node 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: lts/* 37 | cache: "npm" 38 | - name: Install dependencies 39 | run: npm ci 40 | - name: Build 41 | run: npm run build 42 | - name: Setup Pages 43 | uses: actions/configure-pages@v5 44 | - name: Upload artifact 45 | uses: actions/upload-pages-artifact@v3 46 | with: 47 | # Upload dist folder 48 | path: "./dist" 49 | - name: Deploy to GitHub Pages 50 | id: deployment 51 | uses: actions/deploy-pages@v4 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | - "12" 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## ISC License 2 | 3 | Copyright (c) 2016, Per Liedman (per@liedman.net) 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GeoJSON Path Finder 2 | 3 | [![Build status](https://travis-ci.org/perliedman/geojson-path-finder.svg?branch=master)](https://travis-ci.org/perliedman/geojson-path-finder) 4 | 5 | Find shortest paths through a network of GeoJSON. 6 | 7 | Given a network of GeoJSON `LineString`s, GeoJSON Path Finder will find the shortest path between two points in the network. This might be useful for automatic route searches in smaller networks, where setting up a real route planner like OSRM is too much work, or you simply need to do everything on the client. 8 | 9 | See the [GeoJSON Path Finder demo](https://www.liedman.net/geojson-path-finder/). 10 | 11 | _Upgrade notice_ Version 2.0 has been released, which is a TypeScript rewrite - you can still use the module from plain JavaScript, of course. This version also contains some breaking changes regarding option naming; for most common use cases, everything will work as before. 12 | 13 | Breaking changes: 14 | 15 | - option `precision` is now named `tolerance` 16 | - option `keyFn` is now named `key` 17 | - option `weightFn` is now named `weight` 18 | - option `edgeDataReduceFn` is now named `edgeDataReducer` 19 | - option `edgeDataSeed` is now _a function_ taking the properties of the start node 20 | 21 | ## Installing 22 | 23 | ``` 24 | npm install --save geojson-path-finder 25 | ``` 26 | 27 | ## API 28 | 29 | Detailed (and somewhat experimental) [API Docs](https://www.liedman.net/geojson-path-finder/docs/) 30 | 31 | Create a path finding object: 32 | 33 | ```javascript 34 | import PathFinder from "geojson-path-finder"; 35 | import geojson from "./network.json"; 36 | 37 | const pathFinder = new PathFinder(geojson); 38 | ``` 39 | 40 | The GeoJSON object should be a `FeatureCollection` of `LineString` features. The network will be built 41 | into a topology, so that lines that start and end, or cross, at the same coordinate are joined such that 42 | you can find a path from one feature to the other. 43 | 44 | To find the shortest path between two coordinates: 45 | 46 | ```javascript 47 | var path = pathFinder.findPath(start, finish); 48 | ``` 49 | 50 | Where `start` and `finish` are two GeoJSON `point` features. Note that both points _have to_ be vertices in the routing network; if they are not, no route will be found. 51 | 52 | If a route can be found, an object with two properties: `path` and `weight` is returned, where `path` 53 | is the coordinates the path runs through, and `weight` is the total weight (distance in kilometers, if you use the default weight function) of the path. 54 | 55 | As a convenience, the function `pathToGeoJSON` is also exported, it converts the result of a `findPath` call to 56 | a GeoJSON linestring: 57 | 58 | ```javascript 59 | import PathFinder, { pathToGeoJSON } from "geojson-path-finder"; 60 | const pathFinder = new PathFinder(geojson); 61 | const pathLineString = pathToGeoJSON(pathFinder.findPath(start, finish)); 62 | ``` 63 | 64 | (If `findPath` does not find a path, pathToGeoJSON will also return `undefined`.) 65 | 66 | ### `PathFinder` options 67 | 68 | The `PathFinder` constructor takes an optional seconds parameter containing `options` that you can 69 | use to control the behaviour of the path finder. Available options: 70 | 71 | - `weight` controls how the weight (or cost) of travelling between two vertices is calculated; 72 | by default, the geographic distance between the coordinates is calculated and used as weight; 73 | see [Weight functions](#weight-functions) below for details 74 | - `tolerance` (default `1e-5`) controls the tolerance for how close vertices in the GeoJSON can be 75 | before considered being the same vertice; you can say that coordinates closer than this will be 76 | snapped together into one coordinate 77 | - `edgeDataReducer` can optionally be used to store data present in the GeoJSON on each edge of 78 | the routing graph; typically, this can be used for storing things like street names; if specified, 79 | the reduced data is present on found paths under the `edgeDatas` property 80 | - `edgeDataSeed` is a function returning taking a network feature's `properties` as argument and returning the seed used when reducing edge data with the `edgeDataReducer` above 81 | 82 | ## Weight functions 83 | 84 | By default, the _cost_ of going from one node in the network to another is determined simply by 85 | the geographic distance between the two nodes. This means that, by default, shortest paths will be found. 86 | You can however override this by providing a cost calculation function through the `weight` option: 87 | 88 | ```javascript 89 | const pathFinder = new PathFinder(geojson, { 90 | weight: function (a, b, props) { 91 | const dx = a[0] - b[0]; 92 | const dy = a[1] - b[1]; 93 | return Math.sqrt(dx * dx + dy * dy); 94 | }, 95 | }); 96 | ``` 97 | 98 | The weight function is passed two coordinate arrays (in GeoJSON axis order), as well as the feature properties 99 | that are associated with this feature, and should return either: 100 | 101 | - a numeric value for the cost of travelling between the two coordinates; in this case, the cost is assumed 102 | to be the same going from `a` to `b` as going from `b` to `a`; as cost of `0` means the edge can't be used 103 | - an object with two properties: `forward` and `backward`; in this case, 104 | `forward` denotes the cost of going from `a` to `b`, and 105 | `backward` the cost of going from `b` to `a`; setting either 106 | to `0` will prevent taking that direction, the segment will be a oneway. 107 | - `undefined` is the same as setting the weight to `0`: this edge can't be used 108 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #001080; 3 | --dark-hl-0: #9CDCFE; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #0000FF; 7 | --dark-hl-2: #569CD6; 8 | --light-hl-3: #795E26; 9 | --dark-hl-3: #DCDCAA; 10 | --light-hl-4: #A31515; 11 | --dark-hl-4: #CE9178; 12 | --light-hl-5: #098658; 13 | --dark-hl-5: #B5CEA8; 14 | --light-hl-6: #AF00DB; 15 | --dark-hl-6: #C586C0; 16 | --light-code-background: #FFFFFF; 17 | --dark-code-background: #1E1E1E; 18 | } 19 | 20 | @media (prefers-color-scheme: light) { :root { 21 | --hl-0: var(--light-hl-0); 22 | --hl-1: var(--light-hl-1); 23 | --hl-2: var(--light-hl-2); 24 | --hl-3: var(--light-hl-3); 25 | --hl-4: var(--light-hl-4); 26 | --hl-5: var(--light-hl-5); 27 | --hl-6: var(--light-hl-6); 28 | --code-background: var(--light-code-background); 29 | } } 30 | 31 | @media (prefers-color-scheme: dark) { :root { 32 | --hl-0: var(--dark-hl-0); 33 | --hl-1: var(--dark-hl-1); 34 | --hl-2: var(--dark-hl-2); 35 | --hl-3: var(--dark-hl-3); 36 | --hl-4: var(--dark-hl-4); 37 | --hl-5: var(--dark-hl-5); 38 | --hl-6: var(--dark-hl-6); 39 | --code-background: var(--dark-code-background); 40 | } } 41 | 42 | :root[data-theme='light'] { 43 | --hl-0: var(--light-hl-0); 44 | --hl-1: var(--light-hl-1); 45 | --hl-2: var(--light-hl-2); 46 | --hl-3: var(--light-hl-3); 47 | --hl-4: var(--light-hl-4); 48 | --hl-5: var(--light-hl-5); 49 | --hl-6: var(--light-hl-6); 50 | --code-background: var(--light-code-background); 51 | } 52 | 53 | :root[data-theme='dark'] { 54 | --hl-0: var(--dark-hl-0); 55 | --hl-1: var(--dark-hl-1); 56 | --hl-2: var(--dark-hl-2); 57 | --hl-3: var(--dark-hl-3); 58 | --hl-4: var(--dark-hl-4); 59 | --hl-5: var(--dark-hl-5); 60 | --hl-6: var(--dark-hl-6); 61 | --code-background: var(--dark-code-background); 62 | } 63 | 64 | .hl-0 { color: var(--hl-0); } 65 | .hl-1 { color: var(--hl-1); } 66 | .hl-2 { color: var(--hl-2); } 67 | .hl-3 { color: var(--hl-3); } 68 | .hl-4 { color: var(--hl-4); } 69 | .hl-5 { color: var(--hl-5); } 70 | .hl-6 { color: var(--hl-6); } 71 | pre, code { background: var(--code-background); } 72 | -------------------------------------------------------------------------------- /docs/assets/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = JSON.parse("{\"kinds\":{\"2\":\"Module\",\"128\":\"Class\",\"512\":\"Constructor\",\"1024\":\"Property\",\"2048\":\"Method\",\"65536\":\"Type literal\",\"4194304\":\"Type alias\"},\"rows\":[{\"kind\":2,\"name\":\"index\",\"url\":\"modules/index.html\",\"classes\":\"tsd-kind-module\"},{\"kind\":128,\"name\":\"default\",\"url\":\"classes/index.default.html\",\"classes\":\"tsd-kind-class tsd-parent-kind-module\",\"parent\":\"index\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/index.default.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class\",\"parent\":\"index.default\"},{\"kind\":1024,\"name\":\"graph\",\"url\":\"classes/index.default.html#graph\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"index.default\"},{\"kind\":1024,\"name\":\"options\",\"url\":\"classes/index.default.html#options\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"index.default\"},{\"kind\":2048,\"name\":\"findPath\",\"url\":\"classes/index.default.html#findPath\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"index.default\"},{\"kind\":2048,\"name\":\"_createPhantom\",\"url\":\"classes/index.default.html#_createPhantom\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"index.default\"},{\"kind\":2048,\"name\":\"_removePhantom\",\"url\":\"classes/index.default.html#_removePhantom\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"index.default\"},{\"kind\":2,\"name\":\"types\",\"url\":\"modules/types.html\",\"classes\":\"tsd-kind-module\"},{\"kind\":4194304,\"name\":\"Key\",\"url\":\"types/types.Key.html\",\"classes\":\"tsd-kind-type-alias tsd-parent-kind-module\",\"parent\":\"types\"},{\"kind\":4194304,\"name\":\"Edge\",\"url\":\"types/types.Edge.html\",\"classes\":\"tsd-kind-type-alias tsd-parent-kind-module\",\"parent\":\"types\"},{\"kind\":4194304,\"name\":\"Topology\",\"url\":\"types/types.Topology.html\",\"classes\":\"tsd-kind-type-alias tsd-parent-kind-module\",\"parent\":\"types\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"types/types.Topology.html#__type\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-type-alias\",\"parent\":\"types.Topology\"},{\"kind\":1024,\"name\":\"vertices\",\"url\":\"types/types.Topology.html#__type.vertices\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"types.Topology.__type\"},{\"kind\":1024,\"name\":\"edges\",\"url\":\"types/types.Topology.html#__type.edges\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"types.Topology.__type\"},{\"kind\":4194304,\"name\":\"Vertex\",\"url\":\"types/types.Vertex.html\",\"classes\":\"tsd-kind-type-alias tsd-parent-kind-module\",\"parent\":\"types\"},{\"kind\":4194304,\"name\":\"Vertices\",\"url\":\"types/types.Vertices.html\",\"classes\":\"tsd-kind-type-alias tsd-parent-kind-module\",\"parent\":\"types\"},{\"kind\":4194304,\"name\":\"Coordinates\",\"url\":\"types/types.Coordinates.html\",\"classes\":\"tsd-kind-type-alias tsd-parent-kind-module\",\"parent\":\"types\"},{\"kind\":4194304,\"name\":\"PathFinderGraph\",\"url\":\"types/types.PathFinderGraph.html\",\"classes\":\"tsd-kind-type-alias tsd-parent-kind-module\",\"parent\":\"types\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"types/types.PathFinderGraph.html#__type\",\"classes\":\"tsd-kind-type-literal tsd-parent-kind-type-alias\",\"parent\":\"types.PathFinderGraph\"},{\"kind\":1024,\"name\":\"vertices\",\"url\":\"types/types.PathFinderGraph.html#__type.vertices\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"types.PathFinderGraph.__type\"},{\"kind\":1024,\"name\":\"edgeData\",\"url\":\"types/types.PathFinderGraph.html#__type.edgeData\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"types.PathFinderGraph.__type\"},{\"kind\":1024,\"name\":\"sourceCoordinates\",\"url\":\"types/types.PathFinderGraph.html#__type.sourceCoordinates\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"types.PathFinderGraph.__type\"},{\"kind\":1024,\"name\":\"compactedVertices\",\"url\":\"types/types.PathFinderGraph.html#__type.compactedVertices\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"types.PathFinderGraph.__type\"},{\"kind\":1024,\"name\":\"compactedCoordinates\",\"url\":\"types/types.PathFinderGraph.html#__type.compactedCoordinates\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"types.PathFinderGraph.__type\"},{\"kind\":1024,\"name\":\"compactedEdges\",\"url\":\"types/types.PathFinderGraph.html#__type.compactedEdges\",\"classes\":\"tsd-kind-property tsd-parent-kind-type-literal\",\"parent\":\"types.PathFinderGraph.__type\"},{\"kind\":4194304,\"name\":\"PathFinderOptions\",\"url\":\"types/types.PathFinderOptions.html\",\"classes\":\"tsd-kind-type-alias tsd-parent-kind-module\",\"parent\":\"types\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"comment\"],\"fieldVectors\":[[\"name/0\",[0,29.267]],[\"comment/0\",[]],[\"name/1\",[1,29.267]],[\"comment/1\",[]],[\"name/2\",[2,29.267]],[\"comment/2\",[]],[\"name/3\",[3,29.267]],[\"comment/3\",[]],[\"name/4\",[4,29.267]],[\"comment/4\",[]],[\"name/5\",[5,29.267]],[\"comment/5\",[]],[\"name/6\",[6,29.267]],[\"comment/6\",[]],[\"name/7\",[7,29.267]],[\"comment/7\",[]],[\"name/8\",[8,29.267]],[\"comment/8\",[]],[\"name/9\",[9,29.267]],[\"comment/9\",[]],[\"name/10\",[10,29.267]],[\"comment/10\",[]],[\"name/11\",[11,29.267]],[\"comment/11\",[]],[\"name/12\",[12,24.159]],[\"comment/12\",[]],[\"name/13\",[13,20.794]],[\"comment/13\",[]],[\"name/14\",[14,29.267]],[\"comment/14\",[]],[\"name/15\",[15,29.267]],[\"comment/15\",[]],[\"name/16\",[13,20.794]],[\"comment/16\",[]],[\"name/17\",[16,29.267]],[\"comment/17\",[]],[\"name/18\",[17,29.267]],[\"comment/18\",[]],[\"name/19\",[12,24.159]],[\"comment/19\",[]],[\"name/20\",[13,20.794]],[\"comment/20\",[]],[\"name/21\",[18,29.267]],[\"comment/21\",[]],[\"name/22\",[19,29.267]],[\"comment/22\",[]],[\"name/23\",[20,29.267]],[\"comment/23\",[]],[\"name/24\",[21,29.267]],[\"comment/24\",[]],[\"name/25\",[22,29.267]],[\"comment/25\",[]],[\"name/26\",[23,29.267]],[\"comment/26\",[]]],\"invertedIndex\":[[\"__type\",{\"_index\":12,\"name\":{\"12\":{},\"19\":{}},\"comment\":{}}],[\"_createphantom\",{\"_index\":6,\"name\":{\"6\":{}},\"comment\":{}}],[\"_removephantom\",{\"_index\":7,\"name\":{\"7\":{}},\"comment\":{}}],[\"compactedcoordinates\",{\"_index\":21,\"name\":{\"24\":{}},\"comment\":{}}],[\"compactededges\",{\"_index\":22,\"name\":{\"25\":{}},\"comment\":{}}],[\"compactedvertices\",{\"_index\":20,\"name\":{\"23\":{}},\"comment\":{}}],[\"constructor\",{\"_index\":2,\"name\":{\"2\":{}},\"comment\":{}}],[\"coordinates\",{\"_index\":16,\"name\":{\"17\":{}},\"comment\":{}}],[\"default\",{\"_index\":1,\"name\":{\"1\":{}},\"comment\":{}}],[\"edge\",{\"_index\":10,\"name\":{\"10\":{}},\"comment\":{}}],[\"edgedata\",{\"_index\":18,\"name\":{\"21\":{}},\"comment\":{}}],[\"edges\",{\"_index\":14,\"name\":{\"14\":{}},\"comment\":{}}],[\"findpath\",{\"_index\":5,\"name\":{\"5\":{}},\"comment\":{}}],[\"graph\",{\"_index\":3,\"name\":{\"3\":{}},\"comment\":{}}],[\"index\",{\"_index\":0,\"name\":{\"0\":{}},\"comment\":{}}],[\"key\",{\"_index\":9,\"name\":{\"9\":{}},\"comment\":{}}],[\"options\",{\"_index\":4,\"name\":{\"4\":{}},\"comment\":{}}],[\"pathfindergraph\",{\"_index\":17,\"name\":{\"18\":{}},\"comment\":{}}],[\"pathfinderoptions\",{\"_index\":23,\"name\":{\"26\":{}},\"comment\":{}}],[\"sourcecoordinates\",{\"_index\":19,\"name\":{\"22\":{}},\"comment\":{}}],[\"topology\",{\"_index\":11,\"name\":{\"11\":{}},\"comment\":{}}],[\"types\",{\"_index\":8,\"name\":{\"8\":{}},\"comment\":{}}],[\"vertex\",{\"_index\":15,\"name\":{\"15\":{}},\"comment\":{}}],[\"vertices\",{\"_index\":13,\"name\":{\"13\":{},\"16\":{},\"20\":{}},\"comment\":{}}]],\"pipeline\":[]}}"); -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | geojson-path-finder
2 |
3 | 10 |
11 |
12 |
13 |
14 |

geojson-path-finder

15 |
16 | 17 |

GeoJSON Path Finder

18 |
19 |

Greenkeeper badge Build status

20 |

Find shortest path through a network of GeoJSON.

21 |

Given a network of GeoJSON LineStrings, GeoJSON Path Finder will find the shortest path between two points in the network. This might be useful for automatic route searches in smaller networks, where setting up a real route planner like OSRM is too much work, 22 | or you simply need to do everything on the client.

23 |

See the GeoJSON Path Finder demo.

24 | 25 | 26 |

Installing

27 |
28 |
npm install --save geojson-path-finder
 29 | 
30 | 31 | 32 |

API

33 |
34 |

Create a path finding object:

35 |
var PathFinder = require('geojson-path-finder'),
geojson = require('./network.json');

var pathFinder = new PathFinder(geojson); 36 |
37 |

The GeoJSON object should be a FeatureCollection of LineString features. The network will be built 38 | into a topology, so that lines that start and end, or cross, at the same coordinate are joined such that 39 | you can find a path from one feature to the other.

40 |

To find the shortest path between two coordinates:

41 |
var path = pathFinder.findPath(start, finish);
 42 | 
43 |

Where start and finish are two GeoJSON point features.

44 |

If a route can be found, an object with two properties: path and weight is returned, where path 45 | is the coordinates the path runs through, and weight is the total weight (distance in kilometers, if you use the default weight function) of the path.

46 | 47 | 48 |

PathFinder options

49 |
50 |

The PathFinder constructor takes an optional seconds parameter containing options that you can 51 | use to control the behaviour of the path finder. Available options:

52 |
    53 |
  • weightFn controls how the weight (or cost) of travelling between two vertices is calculated; 54 | by default, the geographic distance between the coordinates is calculated and used as weight; 55 | see Weight functions below for details
  • 56 |
  • precision (default 1e-5) controls the tolerance for how close vertices in the GeoJSON can be 57 | before considered being the same vertice; you can say that coordinates closer than this will be 58 | snapped together into one coordinate
  • 59 |
  • edgeDataReduceFn can optionally be used to store data present in the GeoJSON on each edge of 60 | the routing graph; typically, this can be used for storing things like street names; if specified, 61 | the reduced data is present on found paths under the edgeDatas property
  • 62 |
  • edgeDataSeed is the seed used when reducing edge data with the edgeDataReduceFn above
  • 63 |
64 | 65 | 66 |

Weight functions

67 |
68 |

By default, the cost of going from one node in the network to another is determined simply by 69 | the geographic distance between the two nodes. This means that, by default, shortest paths will be found. 70 | You can however override this by providing a cost calculation function through the weightFn option:

71 |
var pathFinder = new PathFinder(geojson, {
weightFn: function(a, b, props) {
var dx = a[0] - b[0];
var dy = a[1] - b[1];
return Math.sqrt(dx * dx + dy * dy);
}
}); 72 |
73 |

The weight function is passed two coordinate arrays (in GeoJSON axis order), as well as the feature properties 74 | that are associated with this feature, and should return either:

75 |
    76 |
  • a numeric value for the cost of travelling between the two coordinates; in this case, the cost is assumed 77 | to be the same going from a to b as going from b to a
  • 78 |
  • an object with two properties: forward and backward; in this case, 79 | forward denotes the cost of going from a to b, and 80 | backward the cost of going from b to a; setting either 81 | to 0, null or undefined will prevent taking that direction, 82 | the segment will be a oneway.
  • 83 |
84 |
85 |
108 |
109 |

Generated using TypeDoc

110 |
-------------------------------------------------------------------------------- /docs/modules.html: -------------------------------------------------------------------------------- 1 | geojson-path-finder
2 |
3 | 10 |
11 |
12 |
13 |
14 |

geojson-path-finder

15 |
16 |
17 |

Index

18 |
19 |

Modules

20 |
index 21 | types 22 |
23 |
46 |
47 |

Generated using TypeDoc

48 |
-------------------------------------------------------------------------------- /docs/modules/index.html: -------------------------------------------------------------------------------- 1 | index | geojson-path-finder
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Module index

20 |
21 |
22 |
23 |
24 |

Index

25 |
26 |

Classes

27 |
default 28 |
29 |
55 |
56 |

Generated using TypeDoc

57 |
-------------------------------------------------------------------------------- /docs/modules/types.html: -------------------------------------------------------------------------------- 1 | types | geojson-path-finder
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Module types

20 |
21 |
22 |
23 |
24 |

Index

25 |
26 |

Type Aliases

27 |
Coordinates 28 | Edge 29 | Key 30 | PathFinderGraph 31 | PathFinderOptions 32 | Topology 33 | Vertex 34 | Vertices 35 |
36 |
69 |
70 |

Generated using TypeDoc

71 |
-------------------------------------------------------------------------------- /docs/types/types.Coordinates.html: -------------------------------------------------------------------------------- 1 | Coordinates | geojson-path-finder
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Type alias Coordinates

19 |
Coordinates: Record<Key, Position>
22 |
55 |
56 |

Generated using TypeDoc

57 |
-------------------------------------------------------------------------------- /docs/types/types.Edge.html: -------------------------------------------------------------------------------- 1 | Edge | geojson-path-finder
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Type alias Edge<TProperties>

19 |
Edge<TProperties>: [Key, Key, TProperties]
20 |

Edge from A to B, containing its vertex keys and associated properties

21 |
22 |
23 |

Type Parameters

24 |
    25 |
  • 26 |

    TProperties

29 |
62 |
63 |

Generated using TypeDoc

64 |
-------------------------------------------------------------------------------- /docs/types/types.Key.html: -------------------------------------------------------------------------------- 1 | Key | geojson-path-finder
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Type alias Key

19 |
Key: string
20 |

Vertex key, a unique identifier for a vertex of a graph

21 |
24 |
57 |
58 |

Generated using TypeDoc

59 |
-------------------------------------------------------------------------------- /docs/types/types.PathFinderGraph.html: -------------------------------------------------------------------------------- 1 | PathFinderGraph | geojson-path-finder
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Type alias PathFinderGraph<TEdgeData>

19 |
PathFinderGraph<TEdgeData>: {
    compactedCoordinates: Record<Key, Record<Key, Position[]>>;
    compactedEdges: Record<Key, Record<Key, TEdgeData | undefined>>;
    compactedVertices: Vertices;
    edgeData: Record<Key, Record<Key, TEdgeData | undefined>>;
    sourceCoordinates: Coordinates;
    vertices: Vertices;
}
20 |
21 |

Type Parameters

22 |
    23 |
  • 24 |

    TEdgeData

25 |
26 |

Type declaration

27 |
    28 |
  • 29 |
    compactedCoordinates: Record<Key, Record<Key, Position[]>>
  • 30 |
  • 31 |
    compactedEdges: Record<Key, Record<Key, TEdgeData | undefined>>
  • 32 |
  • 33 |
    compactedVertices: Vertices
  • 34 |
  • 35 |
    edgeData: Record<Key, Record<Key, TEdgeData | undefined>>
  • 36 |
  • 37 |
    sourceCoordinates: Coordinates
  • 38 |
  • 39 |
    vertices: Vertices
42 |
75 |
76 |

Generated using TypeDoc

77 |
-------------------------------------------------------------------------------- /docs/types/types.PathFinderOptions.html: -------------------------------------------------------------------------------- 1 | PathFinderOptions | geojson-path-finder
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Type alias PathFinderOptions<TEdgeReduce, TProperties>

19 |
PathFinderOptions<TEdgeReduce, TProperties>: {
    compact?: boolean;
    key?: ((coordinates: Position) => string);
    progress?: ((type: string, completed: number, total: number) => void);
    tolerance?: number;
    weight?: ((a: Position, b: Position, properties: TProperties) => number | {
        backward: number;
        forward: number;
    } | undefined);
} & ({
    edgeDataReducer: ((seed: TEdgeReduce, modifier: TEdgeReduce) => TEdgeReduce);
    edgeDataSeed: ((properties: TProperties) => TEdgeReduce);
} | {})
20 |
21 |

Type Parameters

22 |
    23 |
  • 24 |

    TEdgeReduce

  • 25 |
  • 26 |

    TProperties

29 |
62 |
63 |

Generated using TypeDoc

64 |
-------------------------------------------------------------------------------- /docs/types/types.Topology.html: -------------------------------------------------------------------------------- 1 | Topology | geojson-path-finder
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Type alias Topology<TProperties>

19 |
Topology<TProperties>: {
    edges: Edge<TProperties>[];
    vertices: Coordinates;
}
20 |

A topology of coordinates and their connecting edges

21 |
22 |
23 |

Type Parameters

24 |
    25 |
  • 26 |

    TProperties

27 |
28 |

Type declaration

29 |
36 |
69 |
70 |

Generated using TypeDoc

71 |
-------------------------------------------------------------------------------- /docs/types/types.Vertex.html: -------------------------------------------------------------------------------- 1 | Vertex | geojson-path-finder
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Type alias Vertex

19 |
Vertex: Record<Key, number>
20 |

A graph vertex, containing the edges 21 | connecting it to other vertices; 22 | edges are described as a lookup withthe target vertex's 23 | key associated to the edge's weight

24 |
27 |
60 |
61 |

Generated using TypeDoc

62 |
-------------------------------------------------------------------------------- /docs/types/types.Vertices.html: -------------------------------------------------------------------------------- 1 | Vertices | geojson-path-finder
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Type alias Vertices

19 |
Vertices: Record<Key, Vertex>
20 |

A set of vertices, indexed by their keys.

21 |
24 |
57 |
58 |

Generated using TypeDoc

59 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geojson-path-finder", 3 | "version": "2.0.2", 4 | "description": "Find shortest path through a network of GeoJSON", 5 | "repository": "git@github.com:perliedman/geojson-path-finder.git", 6 | "homepage": "https://github.com/perliedman/geojson-path-finder", 7 | "module": "dist/esm/index.js", 8 | "main": "dist/cjs/index.js", 9 | "types": "dist/esm/index.d.ts", 10 | "directories": { 11 | "test": "test" 12 | }, 13 | "scripts": { 14 | "build": "npm run build:esm && npm run build:cjs", 15 | "build:esm": "tsc -p tsconfig.json", 16 | "build:cjs": "tsc -p tsconfig.cjs.json", 17 | "prepare": "npm test && npm run build", 18 | "pretest": "npm run build:esm", 19 | "test": "vitest run" 20 | }, 21 | "author": "Per Liedman ", 22 | "license": "ISC", 23 | "dependencies": { 24 | "@turf/distance": "^7.2.0", 25 | "@turf/explode": "^7.2.0", 26 | "@turf/helpers": "^7.2.0", 27 | "tinyqueue": "^2.0.3" 28 | }, 29 | "devDependencies": { 30 | "@tsconfig/node16": "^1.0.3", 31 | "@types/geojson": "^7946.0.15", 32 | "@types/node": "^18.11.7", 33 | "esm": "^3.2.25", 34 | "typedoc": "^0.23.22", 35 | "typescript": "^4.8.4", 36 | "vitest": "^0.26.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/compactor.ts: -------------------------------------------------------------------------------- 1 | import { Position } from "geojson"; 2 | import { Coordinates, PathFinderOptions, Vertices, Key } from "./types"; 3 | 4 | /** 5 | * Given a graph of vertices and edges, simplifies the graph so redundant 6 | * nodes/edges are removed, only preserving nodes which are either: 7 | * 8 | * * Dead ends: end of lines, where you can only go back in the opposite 9 | * direction 10 | * * Forks, where there is an option to go in multiple directions 11 | * 12 | * The idea is to reduce the number of nodes in the graph, which drasticly 13 | * reduces the complexity of Dijkstra's algorithm. 14 | * 15 | * @param sourceVertices the graph's vertices (a lookup of vertex edges and weights) 16 | * @param vertexCoords the geographic coordinates of the vertices 17 | * @param edgeData the (optional) data associated with each edge 18 | * @param options options used for creating and compacting the graph 19 | * @returns 20 | */ 21 | export default function compactGraph( 22 | sourceVertices: Vertices, 23 | vertexCoords: Coordinates, 24 | sourceEdgeData: Record>, 25 | options: PathFinderOptions = {} 26 | ): { 27 | vertices: Vertices; 28 | coordinates: Record>; 29 | edgeData: Record>; 30 | } { 31 | const result = { 32 | vertices: Object.keys(sourceVertices).reduce( 33 | (clonedVertices, vertexKey) => { 34 | clonedVertices[vertexKey] = { ...sourceVertices[vertexKey] }; 35 | return clonedVertices; 36 | }, 37 | {} as Vertices 38 | ), 39 | coordinates: Object.keys(sourceVertices).reduce( 40 | (coordinates, vertexKey) => { 41 | coordinates[vertexKey] = {}; 42 | for (const neighborKey of Object.keys(sourceVertices[vertexKey])) { 43 | coordinates[vertexKey][neighborKey] = [vertexCoords[vertexKey]]; 44 | } 45 | 46 | return coordinates; 47 | }, 48 | {} as Record> 49 | ), 50 | edgeData: 51 | "edgeDataReducer" in options 52 | ? Object.keys(sourceVertices).reduce((compactedEdges, vertexKey) => { 53 | compactedEdges[vertexKey] = Object.keys( 54 | sourceVertices[vertexKey] 55 | ).reduce((compactedEdges, targetKey) => { 56 | compactedEdges[targetKey] = sourceEdgeData[vertexKey][targetKey]; 57 | return compactedEdges; 58 | }, {} as Record); 59 | return compactedEdges; 60 | }, {} as Record>) 61 | : {}, 62 | }; 63 | 64 | const { vertices, coordinates, edgeData } = result; 65 | const hasEdgeDataReducer = "edgeDataReducer" in options && edgeData; 66 | 67 | const vertexKeysToCompact = Object.keys(sourceVertices).filter((vertexKey) => 68 | shouldCompact(sourceVertices, vertexKey) 69 | ); 70 | 71 | for (const vertexKey of vertexKeysToCompact) { 72 | const vertex = vertices[vertexKey]; 73 | const edges = Object.keys(vertex); 74 | 75 | // No edges means all other vertices around this one have been compacted 76 | // and compacting this node would remove this part of the graph; skip compaction. 77 | if (edges.length === 0) continue; 78 | 79 | for (const neighborKey of edges) { 80 | for (const otherNeighborKey of edges) { 81 | if (neighborKey !== otherNeighborKey) { 82 | compact(vertexKey, neighborKey, otherNeighborKey); 83 | compact(vertexKey, otherNeighborKey, neighborKey); 84 | } 85 | } 86 | } 87 | 88 | for (const neighborKey of edges) { 89 | if (!vertices[neighborKey]) { 90 | throw new Error(`Missing neighbor vertex for ${neighborKey}`); 91 | } 92 | delete vertices[neighborKey][vertexKey]; 93 | delete coordinates[neighborKey][vertexKey]; 94 | } 95 | 96 | delete vertices[vertexKey]; 97 | delete coordinates[vertexKey]; 98 | } 99 | 100 | return result; 101 | 102 | function compact(vertexKey: Key, neighborKey: Key, otherNeighborKey: Key) { 103 | const vertex = vertices[vertexKey]; 104 | const neighbor = vertices[neighborKey]; 105 | const weightFromNeighbor = neighbor[vertexKey]; 106 | 107 | if (!neighbor[otherNeighborKey] && weightFromNeighbor) { 108 | neighbor[otherNeighborKey] = 109 | weightFromNeighbor + vertex[otherNeighborKey]; 110 | coordinates[neighborKey][otherNeighborKey] = [ 111 | ...coordinates[neighborKey][vertexKey], 112 | ...coordinates[vertexKey][otherNeighborKey], 113 | ]; 114 | let reducedEdge = hasEdgeDataReducer 115 | ? edgeData[neighborKey][vertexKey] 116 | : undefined; 117 | const otherEdgeData = hasEdgeDataReducer 118 | ? edgeData[vertexKey][otherNeighborKey] 119 | : undefined; 120 | 121 | if (hasEdgeDataReducer && reducedEdge && otherEdgeData) { 122 | edgeData[neighborKey][otherNeighborKey] = options.edgeDataReducer( 123 | reducedEdge, 124 | otherEdgeData 125 | ); 126 | } 127 | } 128 | } 129 | } 130 | 131 | export function compactNode( 132 | key: Key, 133 | vertices: Vertices, 134 | ends: Vertices, 135 | vertexCoords: Coordinates, 136 | edgeData: Record>, 137 | trackIncoming: boolean, 138 | options: PathFinderOptions = {} 139 | ) { 140 | const neighbors = vertices[key]; 141 | return Object.keys(neighbors).reduce(compactEdge, { 142 | edges: {}, 143 | incomingEdges: {}, 144 | coordinates: {}, 145 | incomingCoordinates: {}, 146 | reducedEdges: {}, 147 | }); 148 | 149 | function compactEdge( 150 | result: { 151 | edges: Record; 152 | incomingEdges: Record; 153 | coordinates: Record; 154 | incomingCoordinates: Record; 155 | reducedEdges: Record; 156 | }, 157 | j: Key 158 | ) { 159 | const neighbor = findNextFork( 160 | key, 161 | j, 162 | vertices, 163 | ends, 164 | vertexCoords, 165 | edgeData, 166 | trackIncoming, 167 | options 168 | ); 169 | const weight = neighbor.weight; 170 | const reverseWeight = neighbor.reverseWeight; 171 | if (neighbor.vertexKey !== key) { 172 | if ( 173 | !result.edges[neighbor.vertexKey] || 174 | result.edges[neighbor.vertexKey] > weight 175 | ) { 176 | result.edges[neighbor.vertexKey] = weight; 177 | result.coordinates[neighbor.vertexKey] = [vertexCoords[key]].concat( 178 | neighbor.coordinates 179 | ); 180 | result.reducedEdges[neighbor.vertexKey] = neighbor.reducedEdge; 181 | } 182 | if ( 183 | trackIncoming && 184 | !isNaN(reverseWeight) && 185 | (!result.incomingEdges[neighbor.vertexKey] || 186 | result.incomingEdges[neighbor.vertexKey] > reverseWeight) 187 | ) { 188 | result.incomingEdges[neighbor.vertexKey] = reverseWeight; 189 | var coordinates = [vertexCoords[key]].concat(neighbor.coordinates); 190 | coordinates.reverse(); 191 | result.incomingCoordinates[neighbor.vertexKey] = coordinates; 192 | } 193 | } 194 | return result; 195 | } 196 | } 197 | 198 | function findNextFork( 199 | prev: Key, 200 | vertexKey: Key, 201 | vertices: Vertices, 202 | ends: Vertices, 203 | vertexCoords: Coordinates, 204 | edgeData: Record>, 205 | trackIncoming: boolean, 206 | options: PathFinderOptions = {} 207 | ) { 208 | let weight = vertices[prev][vertexKey]; 209 | let reverseWeight = vertices[vertexKey][prev]; 210 | const coordinates = []; 211 | const path = []; 212 | let reducedEdge = 213 | "edgeDataReducer" in options ? edgeData[vertexKey][prev] : undefined; 214 | 215 | while (!ends[vertexKey]) { 216 | var edges = vertices[vertexKey]; 217 | 218 | if (!edges) { 219 | break; 220 | } 221 | 222 | var next = Object.keys(edges).filter(function notPrevious(k) { 223 | return k !== prev; 224 | })[0]; 225 | weight += edges[next]; 226 | 227 | if (trackIncoming) { 228 | reverseWeight += vertices[next]?.[vertexKey] || Infinity; 229 | 230 | if (path.indexOf(vertexKey) >= 0) { 231 | ends[vertexKey] = vertices[vertexKey]; 232 | break; 233 | } 234 | path.push(vertexKey); 235 | } 236 | 237 | const nextEdgeData = edgeData[vertexKey] && edgeData[vertexKey][next]; 238 | if ("edgeDataReducer" in options && reducedEdge && nextEdgeData) { 239 | reducedEdge = options.edgeDataReducer(reducedEdge, nextEdgeData); 240 | } 241 | 242 | coordinates.push(vertexCoords[vertexKey]); 243 | prev = vertexKey; 244 | vertexKey = next; 245 | } 246 | 247 | return { 248 | vertexKey, 249 | weight: weight, 250 | reverseWeight: reverseWeight, 251 | coordinates: coordinates, 252 | reducedEdge: reducedEdge, 253 | }; 254 | } 255 | 256 | function shouldCompact(vertices: Vertices, vertexKey: Key): boolean { 257 | const vertex = vertices[vertexKey]; 258 | const edges = Object.keys(vertex); 259 | const numberEdges = edges.length; 260 | 261 | switch (numberEdges) { 262 | case 1: { 263 | // A vertex A with a single edge A->B is a fork 264 | // if B has an edge to A. 265 | // (It's a fork in the sense that it is a dead end and you can only turn back to B.) 266 | const other = vertices[edges[0]]; 267 | return !other[vertexKey]; 268 | } 269 | case 2: { 270 | // A vertex A which lies between two vertices B and C (only has two edges) 271 | // is only a fork if you can't go back to A from at least one of them. 272 | return edges.every((n) => vertices[n][vertexKey]); 273 | } 274 | default: 275 | // A vertex with more than two edges (a fork) is always a fork 276 | return false; 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/dijkstra.ts: -------------------------------------------------------------------------------- 1 | import Queue from "tinyqueue"; 2 | import { Key, Vertices } from "./types"; 3 | 4 | type State = [number, Key[], Key]; 5 | 6 | export default function findPath( 7 | graph: Vertices, 8 | start: Key, 9 | end: Key 10 | ): [number, Key[]] | undefined { 11 | const costs: Record = { [start]: 0 }; 12 | const initialState: State = [0, [start], start]; 13 | const queue = new Queue([initialState], (a: State, b: State) => a[0] - b[0]); 14 | 15 | while (true) { 16 | const state = queue.pop(); 17 | if (!state) { 18 | return undefined; 19 | } 20 | 21 | const cost = state[0]; 22 | const node = state[2]; 23 | if (node === end) { 24 | return [state[0], state[1]]; 25 | } 26 | 27 | const neighbours = graph[node]; 28 | Object.keys(neighbours).forEach(function (n) { 29 | var newCost = cost + neighbours[n]; 30 | if (newCost < Infinity && (!(n in costs) || newCost < costs[n])) { 31 | costs[n] = newCost; 32 | const newState: State = [newCost, state[1].concat([n]), n]; 33 | queue.push(newState); 34 | } 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { lineString } from "@turf/helpers"; 2 | import { 3 | Feature, 4 | FeatureCollection, 5 | GeoJsonProperties, 6 | LineString, 7 | Point, 8 | Position, 9 | } from "geojson"; 10 | import { compactNode } from "./compactor"; 11 | import findPath from "./dijkstra"; 12 | import preprocess from "./preprocessor"; 13 | import roundCoord from "./round-coord"; 14 | import { defaultKey } from "./topology"; 15 | import { Key, Path, PathFinderGraph, PathFinderOptions } from "./types"; 16 | 17 | export default class PathFinder< 18 | TEdgeReduce, 19 | TProperties extends GeoJsonProperties 20 | > { 21 | graph: PathFinderGraph; 22 | options: PathFinderOptions; 23 | 24 | constructor( 25 | network: FeatureCollection, 26 | options: PathFinderOptions = {} 27 | ) { 28 | this.graph = preprocess(network, options); 29 | this.options = options; 30 | 31 | // if ( 32 | // Object.keys(this.graph.compactedVertices).filter(function (k) { 33 | // return k !== "edgeData"; 34 | // }).length === 0 35 | // ) { 36 | // throw new Error( 37 | // "Compacted graph contains no forks (topology has no intersections)." 38 | // ); 39 | // } 40 | } 41 | 42 | findPath( 43 | a: Feature, 44 | b: Feature 45 | ): Path | undefined { 46 | const { key = defaultKey, tolerance = 1e-5 } = this.options; 47 | const start = key(roundCoord(a.geometry.coordinates, tolerance)); 48 | const finish = key(roundCoord(b.geometry.coordinates, tolerance)); 49 | 50 | // We can't find a path if start or finish isn't in the 51 | // set of non-compacted vertices 52 | if (!this.graph.vertices[start] || !this.graph.vertices[finish]) { 53 | return undefined; 54 | } 55 | 56 | const phantomStart = this._createPhantom(start); 57 | const phantomEnd = this._createPhantom(finish); 58 | try { 59 | const pathResult = findPath(this.graph.compactedVertices, start, finish); 60 | 61 | if (pathResult) { 62 | const [weight, path] = pathResult; 63 | return { 64 | path: path 65 | .reduce( 66 | ( 67 | coordinates: Position[], 68 | vertexKey: Key, 69 | index: number, 70 | vertexKeys: Key[] 71 | ) => { 72 | if (index > 0) { 73 | coordinates = coordinates.concat( 74 | this.graph.compactedCoordinates[vertexKeys[index - 1]][ 75 | vertexKey 76 | ] 77 | ); 78 | } 79 | 80 | return coordinates; 81 | }, 82 | [] 83 | ) 84 | .concat([this.graph.sourceCoordinates[finish]]), 85 | weight, 86 | edgeDatas: 87 | "edgeDataReducer" in this.options 88 | ? path.reduce( 89 | ( 90 | edges: (TEdgeReduce | undefined)[], 91 | vertexKey: Key, 92 | index: number, 93 | vertexKeys: Key[] 94 | ) => { 95 | if (index > 0) { 96 | edges.push( 97 | this.graph.compactedEdges[vertexKeys[index - 1]][ 98 | vertexKey 99 | ] 100 | ); 101 | } 102 | 103 | return edges; 104 | }, 105 | [] 106 | ) 107 | : undefined, 108 | }; 109 | } else { 110 | return undefined; 111 | } 112 | } finally { 113 | this._removePhantom(phantomStart); 114 | this._removePhantom(phantomEnd); 115 | } 116 | } 117 | 118 | _createPhantom(n: Key) { 119 | if (this.graph.compactedVertices[n]) return undefined; 120 | 121 | const phantom = compactNode( 122 | n, 123 | this.graph.vertices, 124 | this.graph.compactedVertices, 125 | this.graph.sourceCoordinates, 126 | this.graph.edgeData, 127 | true, 128 | this.options 129 | ); 130 | this.graph.compactedVertices[n] = phantom.edges; 131 | this.graph.compactedCoordinates[n] = phantom.coordinates; 132 | 133 | if ("edgeDataReducer" in this.options) { 134 | this.graph.compactedEdges[n] = phantom.reducedEdges; 135 | } 136 | 137 | Object.keys(phantom.incomingEdges).forEach((neighbor) => { 138 | this.graph.compactedVertices[neighbor][n] = 139 | phantom.incomingEdges[neighbor]; 140 | if (!this.graph.compactedCoordinates[neighbor]) { 141 | this.graph.compactedCoordinates[neighbor] = {}; 142 | } 143 | this.graph.compactedCoordinates[neighbor][n] = [ 144 | this.graph.sourceCoordinates[neighbor], 145 | ...phantom.incomingCoordinates[neighbor].slice(0, -1), 146 | ]; 147 | if (this.graph.compactedEdges) { 148 | if (!this.graph.compactedEdges[neighbor]) { 149 | this.graph.compactedEdges[neighbor] = {}; 150 | } 151 | this.graph.compactedEdges[neighbor][n] = phantom.reducedEdges[neighbor]; 152 | } 153 | }); 154 | 155 | return n; 156 | } 157 | 158 | _removePhantom(n: Key | undefined) { 159 | if (!n) return; 160 | 161 | Object.keys(this.graph.compactedVertices[n]).forEach((neighbor) => { 162 | delete this.graph.compactedVertices[neighbor][n]; 163 | }); 164 | Object.keys(this.graph.compactedCoordinates[n]).forEach((neighbor) => { 165 | delete this.graph.compactedCoordinates[neighbor][n]; 166 | }); 167 | if ("edgeDataReducer" in this.options) { 168 | Object.keys(this.graph.compactedEdges[n]).forEach((neighbor) => { 169 | delete this.graph.compactedEdges[neighbor][n]; 170 | }); 171 | } 172 | 173 | delete this.graph.compactedVertices[n]; 174 | delete this.graph.compactedCoordinates[n]; 175 | 176 | if (this.graph.compactedEdges) { 177 | delete this.graph.compactedEdges[n]; 178 | } 179 | } 180 | } 181 | 182 | export function pathToGeoJSON( 183 | path: Path | undefined 184 | ): 185 | | Feature< 186 | LineString, 187 | { weight: number; edgeDatas: (TEdgeReduce | undefined)[] | undefined } 188 | > 189 | | undefined { 190 | if (path) { 191 | const { weight, edgeDatas } = path; 192 | return lineString(path.path, { weight, edgeDatas }); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/preprocessor.ts: -------------------------------------------------------------------------------- 1 | import distance from "@turf/distance"; 2 | import { point } from "@turf/helpers"; 3 | import { 4 | FeatureCollection, 5 | GeoJsonProperties, 6 | Geometry, 7 | Position, 8 | } from "geojson"; 9 | import type { 10 | PathFinderGraph, 11 | PathFinderOptions, 12 | Edge, 13 | Key, 14 | Vertices, 15 | } from "./types"; 16 | import compactGraph from "./compactor"; 17 | import createTopology from "./topology"; 18 | 19 | export default function preprocess< 20 | TEdgeReduce, 21 | TProperties extends GeoJsonProperties 22 | >( 23 | network: FeatureCollection, 24 | options: PathFinderOptions = {} 25 | ): PathFinderGraph { 26 | const topology = createTopology(network, options); 27 | const { weight = defaultWeight } = options; 28 | 29 | const graph = topology.edges.reduce(reduceEdges, { 30 | edgeData: {}, 31 | vertices: {}, 32 | } as PathFinderGraph); 33 | 34 | const { 35 | vertices: compactedVertices, 36 | coordinates: compactedCoordinates, 37 | edgeData: compactedEdges, 38 | } = compactGraph(graph.vertices, topology.vertices, graph.edgeData, options); 39 | 40 | return { 41 | vertices: graph.vertices, 42 | edgeData: graph.edgeData, 43 | sourceCoordinates: topology.vertices, 44 | compactedVertices, 45 | compactedCoordinates, 46 | compactedEdges, 47 | }; 48 | 49 | function reduceEdges( 50 | g: PathFinderGraph, 51 | edge: Edge, 52 | i: number, 53 | es: Edge[] 54 | ) { 55 | const [a, b, properties] = edge; 56 | const w = weight(topology.vertices[a], topology.vertices[b], properties); 57 | 58 | if (w) { 59 | makeEdgeList(a); 60 | makeEdgeList(b); 61 | // If the weight for an edge is falsy, it means the edge is impassable; 62 | // we still add the edge to the graph, but with a weight of Infinity, 63 | // since this makes compaction easier. 64 | // After compaction, we remove any edge with a weight of Infinity. 65 | if (w instanceof Object) { 66 | concatEdge(a, b, w.forward || Infinity); 67 | concatEdge(b, a, w.backward || Infinity); 68 | } else { 69 | concatEdge(a, b, w || Infinity); 70 | concatEdge(b, a, w || Infinity); 71 | } 72 | } 73 | 74 | if (i % 1000 === 0 && options.progress) { 75 | options.progress("edgeweights", i, es.length); 76 | } 77 | 78 | return g; 79 | 80 | function makeEdgeList(node: Key) { 81 | if (!g.vertices[node]) { 82 | g.vertices[node] = {}; 83 | g.edgeData[node] = {}; 84 | } 85 | } 86 | 87 | function concatEdge(startNode: Key, endNode: Key, weight: number) { 88 | var v = g.vertices[startNode]; 89 | v[endNode] = weight; 90 | g.edgeData[startNode][endNode] = 91 | "edgeDataReducer" in options 92 | ? options.edgeDataSeed(properties) 93 | : undefined; 94 | } 95 | } 96 | } 97 | 98 | function defaultWeight(a: Position, b: Position) { 99 | return distance(point(a), point(b)); 100 | } 101 | -------------------------------------------------------------------------------- /src/round-coord.ts: -------------------------------------------------------------------------------- 1 | import { Position } from "geojson"; 2 | 3 | export default function roundCoord( 4 | coord: Position, 5 | tolerance: number 6 | ): Position { 7 | return [ 8 | Math.round(coord[0] / tolerance) * tolerance, 9 | Math.round(coord[1] / tolerance) * tolerance, 10 | ]; 11 | } 12 | -------------------------------------------------------------------------------- /src/topology.ts: -------------------------------------------------------------------------------- 1 | import { featureCollection } from "@turf/helpers"; 2 | import { 3 | GeoJSON, 4 | Feature, 5 | FeatureCollection, 6 | LineString, 7 | Position, 8 | Geometry, 9 | GeoJsonProperties, 10 | } from "geojson"; 11 | import explode from "@turf/explode"; 12 | import roundCoord from "./round-coord"; 13 | import { Edge, PathFinderOptions, Topology } from "./types"; 14 | 15 | export default function createTopology< 16 | TEdgeData, 17 | TProperties extends GeoJsonProperties 18 | >( 19 | network: FeatureCollection, 20 | options: PathFinderOptions = {} 21 | ): Topology { 22 | const { key = defaultKey } = options; 23 | const { tolerance = 1e-5 } = options; 24 | const lineStrings = featureCollection( 25 | network.features.filter( 26 | (f): f is Feature => 27 | f.geometry.type === "LineString" 28 | ) 29 | ); 30 | const points = explode(lineStrings as GeoJSON); 31 | const vertices = points.features.reduce(function buildTopologyVertices( 32 | coordinates, 33 | feature, 34 | index, 35 | features 36 | ) { 37 | var rc = roundCoord(feature.geometry.coordinates, tolerance); 38 | coordinates[key(rc)] = feature.geometry.coordinates; 39 | 40 | if (index % 1000 === 0 && options.progress) { 41 | options.progress("topo:vertices", index, features.length); 42 | } 43 | 44 | return coordinates; 45 | }, 46 | {} as Record); 47 | const edges = geoJsonReduce( 48 | lineStrings, 49 | buildTopologyEdges, 50 | [] as Edge[] 51 | ); 52 | 53 | return { 54 | vertices: vertices, 55 | edges: edges, 56 | }; 57 | 58 | function buildTopologyEdges( 59 | edges: Edge[], 60 | f: Feature 61 | ) { 62 | f.geometry.coordinates.forEach(function buildLineStringEdges(c, i, cs) { 63 | if (i > 0) { 64 | var k1 = key(roundCoord(cs[i - 1], tolerance)), 65 | k2 = key(roundCoord(c, tolerance)); 66 | edges.push([k1, k2, f.properties]); 67 | } 68 | }); 69 | 70 | return edges; 71 | } 72 | } 73 | 74 | function geoJsonReduce( 75 | geojson: FeatureCollection | Feature, 76 | fn: (accumulator: T, feature: Feature) => T, 77 | seed: T 78 | ): T { 79 | if (geojson.type === "FeatureCollection") { 80 | return geojson.features.reduce(function reduceFeatures(a, f) { 81 | return geoJsonReduce(f, fn, a); 82 | }, seed); 83 | } else { 84 | return fn(seed, geojson); 85 | } 86 | } 87 | 88 | export function defaultKey(c: Position) { 89 | return c.join(","); 90 | } 91 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Position } from "geojson"; 2 | 3 | /** 4 | * Vertex key, a unique identifier for a vertex of a graph 5 | */ 6 | export type Key = string; 7 | 8 | /** 9 | * Edge from A to B, containing its vertex keys and associated properties 10 | */ 11 | export type Edge = [Key, Key, TProperties]; 12 | 13 | /** 14 | * A topology of coordinates and their connecting edges 15 | */ 16 | export type Topology = { 17 | vertices: Coordinates; 18 | edges: Edge[]; 19 | }; 20 | 21 | /** 22 | * A graph vertex, containing the edges 23 | * connecting it to other vertices; 24 | * edges are described as a lookup withthe target vertex's 25 | * key associated to the edge's weight 26 | */ 27 | export type Vertex = Record; 28 | 29 | /** 30 | * A set of vertices, indexed by their keys. 31 | */ 32 | export type Vertices = Record; 33 | 34 | /** 35 | * 36 | */ 37 | export type Coordinates = Record; 38 | 39 | export type PathFinderGraph = { 40 | vertices: Vertices; 41 | edgeData: Record>; 42 | sourceCoordinates: Coordinates; 43 | compactedVertices: Vertices; 44 | compactedCoordinates: Record>; 45 | compactedEdges: Record>; 46 | }; 47 | 48 | export type PathFinderOptions = { 49 | tolerance?: number; 50 | key?: (coordinates: Position) => string; 51 | compact?: boolean; 52 | /** 53 | * Calculate weight for an edge from a node at position a to a node at position b 54 | * @param {Position} a coordinate of node A 55 | * @param {Position} b coordinate of node B 56 | * @param {Properties} properties the properties associated with the network's LineString from a to b 57 | * @returns the weight of the edge, zero indicates the edge is not passable 58 | */ 59 | weight?: ( 60 | a: Position, 61 | b: Position, 62 | properties: TProperties 63 | ) => number | { forward: number; backward: number } | undefined; 64 | progress?: (type: string, completed: number, total: number) => void; 65 | } & ( 66 | | { 67 | edgeDataReducer: ( 68 | seed: TEdgeReduce, 69 | modifier: TEdgeReduce 70 | ) => TEdgeReduce; 71 | edgeDataSeed: (properties: TProperties) => TEdgeReduce; 72 | } 73 | | {} 74 | ); 75 | 76 | export type Path = { 77 | path: Position[]; 78 | weight: number; 79 | edgeDatas: (TEdgeReduce | undefined)[] | undefined; 80 | }; 81 | -------------------------------------------------------------------------------- /test/66.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "id": 1, 7 | "properties": {}, 8 | "geometry": { 9 | "type": "LineString", 10 | "coordinates": [ 11 | [ 12 | 0, 13 | 0 14 | ], 15 | [ 16 | 12, 17 | 0 18 | ] 19 | ] 20 | } 21 | }, 22 | { 23 | "type": "Feature", 24 | "id": 2, 25 | "properties": {}, 26 | "geometry": { 27 | "type": "LineString", 28 | "coordinates": [ 29 | [ 30 | 12, 31 | 0 32 | ], 33 | [ 34 | 12, 35 | 3 36 | ] 37 | ] 38 | } 39 | }, 40 | { 41 | "type": "Feature", 42 | "id": 3, 43 | "properties": {}, 44 | "geometry": { 45 | "type": "LineString", 46 | "coordinates": [ 47 | [ 48 | 12, 49 | 3 50 | ], 51 | [ 52 | 3, 53 | 3 54 | ] 55 | ] 56 | } 57 | }, 58 | { 59 | "type": "Feature", 60 | "id": 4, 61 | "properties": {}, 62 | "geometry": { 63 | "type": "LineString", 64 | "coordinates": [ 65 | [ 66 | 3, 67 | 3 68 | ], 69 | [ 70 | 3, 71 | 6 72 | ] 73 | ] 74 | } 75 | }, 76 | { 77 | "type": "Feature", 78 | "id": 5, 79 | "properties": {}, 80 | "geometry": { 81 | "type": "LineString", 82 | "coordinates": [ 83 | [ 84 | 3, 85 | 6 86 | ], 87 | [ 88 | 7, 89 | 6 90 | ] 91 | ] 92 | } 93 | }, 94 | { 95 | "type": "Feature", 96 | "id": 6, 97 | "properties": {}, 98 | "geometry": { 99 | "type": "LineString", 100 | "coordinates": [ 101 | [ 102 | 7, 103 | 6 104 | ], 105 | [ 106 | 7, 107 | 12 108 | ] 109 | ] 110 | } 111 | }, 112 | { 113 | "type": "Feature", 114 | "id": 7, 115 | "properties": {}, 116 | "geometry": { 117 | "type": "LineString", 118 | "coordinates": [ 119 | [ 120 | 7, 121 | 12 122 | ], 123 | [ 124 | 3, 125 | 14 126 | ] 127 | ] 128 | } 129 | }, 130 | { 131 | "type": "Feature", 132 | "id": 8, 133 | "properties": {}, 134 | "geometry": { 135 | "type": "LineString", 136 | "coordinates": [ 137 | [ 138 | 0, 139 | 0 140 | ], 141 | [ 142 | 1, 143 | 2 144 | ] 145 | ] 146 | } 147 | }, 148 | { 149 | "type": "Feature", 150 | "id": 9, 151 | "properties": {}, 152 | "geometry": { 153 | "type": "LineString", 154 | "coordinates": [ 155 | [ 156 | 1, 157 | 2 158 | ], 159 | [ 160 | 3, 161 | 3 162 | ] 163 | ] 164 | } 165 | }, 166 | { 167 | "type": "Feature", 168 | "id": 10, 169 | "properties": {}, 170 | "geometry": { 171 | "type": "LineString", 172 | "coordinates": [ 173 | [ 174 | 9, 175 | 6 176 | ], 177 | [ 178 | 12, 179 | 3 180 | ] 181 | ] 182 | } 183 | }, 184 | { 185 | "type": "Feature", 186 | "id": 13, 187 | "properties": {}, 188 | "geometry": { 189 | "type": "LineString", 190 | "coordinates": [ 191 | [ 192 | 7, 193 | 12 194 | ], 195 | [ 196 | 15, 197 | 12 198 | ] 199 | ] 200 | } 201 | }, 202 | { 203 | "type": "Feature", 204 | "id": 11, 205 | "properties": {}, 206 | "geometry": { 207 | "type": "LineString", 208 | "coordinates": [ 209 | [ 210 | 7, 211 | 6 212 | ], 213 | [ 214 | 9, 215 | 6 216 | ] 217 | ] 218 | } 219 | }, 220 | { 221 | "type": "Feature", 222 | "id": 12, 223 | "properties": {}, 224 | "geometry": { 225 | "type": "LineString", 226 | "coordinates": [ 227 | [ 228 | 9, 229 | 6 230 | ], 231 | [ 232 | 15, 233 | 12 234 | ] 235 | ] 236 | } 237 | } 238 | ] 239 | } -------------------------------------------------------------------------------- /test/advent24.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "geometry": { 7 | "type": "LineString", 8 | "coordinates": [ 9 | [ 10 | 1, 11 | 1 12 | ], 13 | [ 14 | 2, 15 | 1 16 | ], 17 | [ 18 | 3, 19 | 1 20 | ], 21 | [ 22 | 4, 23 | 1 24 | ], 25 | [ 26 | 5, 27 | 1 28 | ], 29 | [ 30 | 6, 31 | 1 32 | ], 33 | [ 34 | 7, 35 | 1 36 | ], 37 | [ 38 | 8, 39 | 1 40 | ], 41 | [ 42 | 9, 43 | 1 44 | ] 45 | ] 46 | } 47 | }, 48 | { 49 | "type": "Feature", 50 | "geometry": { 51 | "type": "LineString", 52 | "coordinates": [ 53 | [ 54 | 1, 55 | 2 56 | ] 57 | ] 58 | } 59 | }, 60 | { 61 | "type": "Feature", 62 | "geometry": { 63 | "type": "LineString", 64 | "coordinates": [ 65 | [ 66 | 9, 67 | 2 68 | ] 69 | ] 70 | } 71 | }, 72 | { 73 | "type": "Feature", 74 | "geometry": { 75 | "type": "LineString", 76 | "coordinates": [ 77 | [ 78 | 1, 79 | 3 80 | ], 81 | [ 82 | 2, 83 | 3 84 | ], 85 | [ 86 | 3, 87 | 3 88 | ], 89 | [ 90 | 4, 91 | 3 92 | ], 93 | [ 94 | 5, 95 | 3 96 | ], 97 | [ 98 | 6, 99 | 3 100 | ], 101 | [ 102 | 7, 103 | 3 104 | ], 105 | [ 106 | 8, 107 | 3 108 | ], 109 | [ 110 | 9, 111 | 3 112 | ] 113 | ] 114 | } 115 | }, 116 | { 117 | "type": "Feature", 118 | "geometry": { 119 | "type": "LineString", 120 | "coordinates": [ 121 | [ 122 | 0, 123 | 5 124 | ] 125 | ] 126 | } 127 | }, 128 | { 129 | "type": "Feature", 130 | "geometry": { 131 | "type": "LineString", 132 | "coordinates": [ 133 | [ 134 | 1, 135 | 1 136 | ], 137 | [ 138 | 1, 139 | 2 140 | ], 141 | [ 142 | 1, 143 | 3 144 | ] 145 | ] 146 | } 147 | }, 148 | { 149 | "type": "Feature", 150 | "geometry": { 151 | "type": "LineString", 152 | "coordinates": [ 153 | [ 154 | 1, 155 | 5 156 | ] 157 | ] 158 | } 159 | }, 160 | { 161 | "type": "Feature", 162 | "geometry": { 163 | "type": "LineString", 164 | "coordinates": [ 165 | [ 166 | 2, 167 | 1 168 | ] 169 | ] 170 | } 171 | }, 172 | { 173 | "type": "Feature", 174 | "geometry": { 175 | "type": "LineString", 176 | "coordinates": [ 177 | [ 178 | 2, 179 | 3 180 | ] 181 | ] 182 | } 183 | }, 184 | { 185 | "type": "Feature", 186 | "geometry": { 187 | "type": "LineString", 188 | "coordinates": [ 189 | [ 190 | 2, 191 | 5 192 | ] 193 | ] 194 | } 195 | }, 196 | { 197 | "type": "Feature", 198 | "geometry": { 199 | "type": "LineString", 200 | "coordinates": [ 201 | [ 202 | 3, 203 | 1 204 | ] 205 | ] 206 | } 207 | }, 208 | { 209 | "type": "Feature", 210 | "geometry": { 211 | "type": "LineString", 212 | "coordinates": [ 213 | [ 214 | 3, 215 | 3 216 | ] 217 | ] 218 | } 219 | }, 220 | { 221 | "type": "Feature", 222 | "geometry": { 223 | "type": "LineString", 224 | "coordinates": [ 225 | [ 226 | 3, 227 | 5 228 | ] 229 | ] 230 | } 231 | }, 232 | { 233 | "type": "Feature", 234 | "geometry": { 235 | "type": "LineString", 236 | "coordinates": [ 237 | [ 238 | 4, 239 | 1 240 | ] 241 | ] 242 | } 243 | }, 244 | { 245 | "type": "Feature", 246 | "geometry": { 247 | "type": "LineString", 248 | "coordinates": [ 249 | [ 250 | 4, 251 | 3 252 | ] 253 | ] 254 | } 255 | }, 256 | { 257 | "type": "Feature", 258 | "geometry": { 259 | "type": "LineString", 260 | "coordinates": [ 261 | [ 262 | 4, 263 | 5 264 | ] 265 | ] 266 | } 267 | }, 268 | { 269 | "type": "Feature", 270 | "geometry": { 271 | "type": "LineString", 272 | "coordinates": [ 273 | [ 274 | 5, 275 | 1 276 | ] 277 | ] 278 | } 279 | }, 280 | { 281 | "type": "Feature", 282 | "geometry": { 283 | "type": "LineString", 284 | "coordinates": [ 285 | [ 286 | 5, 287 | 3 288 | ] 289 | ] 290 | } 291 | }, 292 | { 293 | "type": "Feature", 294 | "geometry": { 295 | "type": "LineString", 296 | "coordinates": [ 297 | [ 298 | 5, 299 | 5 300 | ] 301 | ] 302 | } 303 | }, 304 | { 305 | "type": "Feature", 306 | "geometry": { 307 | "type": "LineString", 308 | "coordinates": [ 309 | [ 310 | 6, 311 | 1 312 | ] 313 | ] 314 | } 315 | }, 316 | { 317 | "type": "Feature", 318 | "geometry": { 319 | "type": "LineString", 320 | "coordinates": [ 321 | [ 322 | 6, 323 | 3 324 | ] 325 | ] 326 | } 327 | }, 328 | { 329 | "type": "Feature", 330 | "geometry": { 331 | "type": "LineString", 332 | "coordinates": [ 333 | [ 334 | 6, 335 | 5 336 | ] 337 | ] 338 | } 339 | }, 340 | { 341 | "type": "Feature", 342 | "geometry": { 343 | "type": "LineString", 344 | "coordinates": [ 345 | [ 346 | 7, 347 | 1 348 | ] 349 | ] 350 | } 351 | }, 352 | { 353 | "type": "Feature", 354 | "geometry": { 355 | "type": "LineString", 356 | "coordinates": [ 357 | [ 358 | 7, 359 | 3 360 | ] 361 | ] 362 | } 363 | }, 364 | { 365 | "type": "Feature", 366 | "geometry": { 367 | "type": "LineString", 368 | "coordinates": [ 369 | [ 370 | 7, 371 | 5 372 | ] 373 | ] 374 | } 375 | }, 376 | { 377 | "type": "Feature", 378 | "geometry": { 379 | "type": "LineString", 380 | "coordinates": [ 381 | [ 382 | 8, 383 | 1 384 | ] 385 | ] 386 | } 387 | }, 388 | { 389 | "type": "Feature", 390 | "geometry": { 391 | "type": "LineString", 392 | "coordinates": [ 393 | [ 394 | 8, 395 | 3 396 | ] 397 | ] 398 | } 399 | }, 400 | { 401 | "type": "Feature", 402 | "geometry": { 403 | "type": "LineString", 404 | "coordinates": [ 405 | [ 406 | 8, 407 | 5 408 | ] 409 | ] 410 | } 411 | }, 412 | { 413 | "type": "Feature", 414 | "geometry": { 415 | "type": "LineString", 416 | "coordinates": [ 417 | [ 418 | 9, 419 | 1 420 | ], 421 | [ 422 | 9, 423 | 2 424 | ], 425 | [ 426 | 9, 427 | 3 428 | ] 429 | ] 430 | } 431 | }, 432 | { 433 | "type": "Feature", 434 | "geometry": { 435 | "type": "LineString", 436 | "coordinates": [ 437 | [ 438 | 9, 439 | 5 440 | ] 441 | ] 442 | } 443 | } 444 | ] 445 | } 446 | -------------------------------------------------------------------------------- /test/compactor.spec.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import compactGraph from "../src/compactor"; 3 | 4 | test("removes redundant vertices 1", () => { 5 | const compacted = compactGraph( 6 | { 7 | "0,0": { "1,0": 1 }, 8 | "1,0": { "0,0": 1, "2,0": 1 }, 9 | "2,0": { "1,0": 1 }, 10 | }, 11 | { 12 | "0,0": [0, 0], 13 | "1,0": [1, 0], 14 | "2,0": [2, 0], 15 | }, 16 | {}, 17 | {} 18 | ); 19 | 20 | expect(compacted.vertices).toEqual({ 21 | "0,0": { "2,0": 2 }, 22 | "2,0": { "0,0": 2 }, 23 | }); 24 | }); 25 | 26 | test("removes redundant vertices 2", () => { 27 | const compacted = compactGraph( 28 | { 29 | "0,0": { "1,0": 1 }, 30 | "1,0": { "0,0": 1, "2,0": 1, "1,1": 1 }, 31 | "1,1": { "1,0": 1 }, 32 | "2,0": { "1,0": 1 }, 33 | }, 34 | { 35 | "0,0": [0, 0], 36 | "1,0": [1, 0], 37 | "1,1": [1, 1], 38 | "2,0": [2, 0], 39 | }, 40 | {}, 41 | {} 42 | ); 43 | 44 | expect(compacted.vertices).toEqual({ 45 | "0,0": { "1,0": 1 }, 46 | "1,0": { "0,0": 1, "2,0": 1, "1,1": 1 }, 47 | "1,1": { "1,0": 1 }, 48 | "2,0": { "1,0": 1 }, 49 | }); 50 | }); 51 | 52 | test("does not remove all vertices from circle", () => { 53 | const compacted = compactGraph( 54 | { 55 | "0,0": { "1,0": 1, "0,1": 1 }, 56 | "1,0": { "0,0": 1, "1,1": 1 }, 57 | "1,1": { "1,0": 1, "0,1": 1 }, 58 | "0,1": { "1,1": 1, "0,0": 1 }, 59 | }, 60 | { 61 | "0,0": [0, 0], 62 | "1,0": [1, 0], 63 | "1,1": [1, 1], 64 | "0,1": [0, 1], 65 | }, 66 | {}, 67 | {} 68 | ); 69 | expect(Object.keys(compacted.vertices).length).toBeGreaterThan(0); 70 | }); 71 | -------------------------------------------------------------------------------- /test/osm-weight.js: -------------------------------------------------------------------------------- 1 | import distance from "@turf/distance"; 2 | import { point } from "@turf/helpers"; 3 | 4 | const highwaySpeeds = { 5 | motorway: 110, 6 | trunk: 90, 7 | primary: 80, 8 | secondary: 70, 9 | tertiary: 50, 10 | unclassified: 50, 11 | road: 50, 12 | residential: 30, 13 | service: 30, 14 | living_street: 20, 15 | }; 16 | 17 | export default function osmWeight(a, b, props) { 18 | var d = distance(point(a), point(b)) * 1000, 19 | factor = 0.9, 20 | type = props.highway, 21 | forwardSpeed, 22 | backwardSpeed; 23 | 24 | if (props.maxspeed) { 25 | forwardSpeed = backwardSpeed = Number(props.maxspeed); 26 | } else { 27 | var linkIndex = type.indexOf("_link"); 28 | if (linkIndex >= 0) { 29 | type = type.substring(0, linkIndex); 30 | factor *= 0.7; 31 | } 32 | 33 | forwardSpeed = backwardSpeed = highwaySpeeds[type] * factor; 34 | } 35 | 36 | if ( 37 | (props.oneway && props.oneway !== "no") || 38 | (props.junction && props.junction === "roundabout") 39 | ) { 40 | backwardSpeed = null; 41 | } 42 | 43 | return { 44 | forward: forwardSpeed && d / (forwardSpeed / 3.6), 45 | backward: backwardSpeed && d / (backwardSpeed / 3.6), 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /test/path.spec.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import PathFinder from "../src/index"; 4 | import geojson from "./network.json"; 5 | import geojson66 from "./66.json"; 6 | import largeNetwork from "./large-network.json"; 7 | import { point } from "@turf/helpers"; 8 | import distance from "@turf/distance"; 9 | import osmWeight from "./osm-weight"; 10 | 11 | test("can create PathFinder", () => { 12 | const pathfinder = new PathFinder(geojson); 13 | expect(pathfinder).toBeTruthy(); 14 | }); 15 | 16 | test("can find path (simple)", () => { 17 | const network = { 18 | type: "FeatureCollection", 19 | features: [ 20 | { 21 | type: "Feature", 22 | geometry: { 23 | type: "LineString", 24 | coordinates: [ 25 | [0, 0], 26 | [1, 0], 27 | ], 28 | }, 29 | }, 30 | { 31 | type: "Feature", 32 | geometry: { 33 | type: "LineString", 34 | coordinates: [ 35 | [1, 0], 36 | [1, 1], 37 | ], 38 | }, 39 | }, 40 | ], 41 | }; 42 | 43 | const pathfinder = new PathFinder(network); 44 | const path = pathfinder.findPath(point([0, 0]), point([1, 1])); 45 | 46 | expect(path).toBeTruthy(); 47 | expect(path.path).toBeTruthy(); 48 | expect(path.path.length).toBe(3); 49 | expect(path.weight).toBeGreaterThan(0); 50 | }); 51 | 52 | test("can find path (medium)", () => { 53 | const network = { 54 | type: "FeatureCollection", 55 | features: [ 56 | { 57 | type: "Feature", 58 | geometry: { 59 | type: "LineString", 60 | coordinates: [ 61 | [0, 0], 62 | [1, 0], 63 | ], 64 | }, 65 | }, 66 | { 67 | type: "Feature", 68 | geometry: { 69 | type: "LineString", 70 | coordinates: [ 71 | [1, 0], 72 | [1, 1], 73 | ], 74 | }, 75 | }, 76 | { 77 | type: "Feature", 78 | geometry: { 79 | type: "LineString", 80 | coordinates: [ 81 | [1, 0], 82 | [0, 1], 83 | [1, 1], 84 | ], 85 | }, 86 | }, 87 | ], 88 | }; 89 | 90 | const pathfinder = new PathFinder(network), 91 | path = pathfinder.findPath(point([0, 0]), point([1, 1])); 92 | 93 | expect(path).toBeTruthy(); 94 | expect(path.path).toBeTruthy(); 95 | expect(path.path.length).toBe(3); 96 | expect(path.weight).toBeGreaterThan(0); 97 | }); 98 | 99 | test("can find path (complex)", () => { 100 | const pathfinder = new PathFinder(geojson), 101 | path = pathfinder.findPath( 102 | point([8.44460166, 59.48947469]), 103 | point([8.44651, 59.513920000000006]) 104 | ); 105 | 106 | expect(path).toBeTruthy(); 107 | expect(path.path).toBeTruthy(); 108 | expect(path.weight).toBeGreaterThan(0); 109 | expect(path.path.length).toBe(220); 110 | expect(path.weight).toBeCloseTo(6.3751); 111 | }); 112 | 113 | test("can handle network without forks", () => { 114 | const pathFinder = new PathFinder(require("./advent24.json"), { 115 | weight: function (a, b) { 116 | const dx = a[0] - b[0]; 117 | const dy = a[1] - b[1]; 118 | return Math.sqrt(dx * dx + dy * dy); 119 | }, 120 | }); 121 | const path = pathFinder.findPath(point([1, 1]), point([9, 1])); 122 | expect(path).toBeTruthy(); 123 | expect(path.path).toBeTruthy(); 124 | expect(path.weight).toBe(8); 125 | }); 126 | 127 | test("can handle multiple path searches in network without forks", () => { 128 | const pathFinder = new PathFinder(require("./advent24.json"), { 129 | weight: function (a, b) { 130 | const dx = a[0] - b[0]; 131 | const dy = a[1] - b[1]; 132 | return Math.sqrt(dx * dx + dy * dy); 133 | }, 134 | }); 135 | 136 | for (let i = 0; i < 2; i++) { 137 | pathFinder.findPath(point([1, 1]), point([9, 1])); 138 | } 139 | }); 140 | 141 | // test("can handle island network", () => { 142 | // const pathFinder = new PathFinder(require("./islands.json")); 143 | // for (let i = 0; i < 2; i++) { 144 | // const path = pathFinder.findPath(point([12.7237479, 55.9095736]), point([12.6766066, 55.9092587])); 145 | // } 146 | // }) 147 | 148 | test("does not remove vertices from result", (t) => { 149 | const pathfinder = new PathFinder(geojson66, { 150 | weight: (a, b) => { 151 | const dx = a[0] - b[0]; 152 | const dy = a[1] - b[1]; 153 | return Math.sqrt(dx * dx + dy * dy); 154 | }, 155 | tolerance: 1, 156 | }), 157 | path = pathfinder.findPath(point([0, 0]), point([15, 12])); 158 | 159 | expect(path).toBeTruthy(); 160 | expect(path.path).toBeTruthy(); 161 | expect(path.weight).toBeGreaterThan(0); 162 | expect(path.path.length).toBe(7); 163 | expect(path.weight).toBeCloseTo(21.9574); 164 | }); 165 | 166 | test("can make oneway network", () => { 167 | const network = { 168 | type: "FeatureCollection", 169 | features: [ 170 | { 171 | type: "Feature", 172 | geometry: { 173 | type: "LineString", 174 | coordinates: [ 175 | [0, 0], 176 | [1, 0], 177 | ], 178 | }, 179 | }, 180 | { 181 | type: "Feature", 182 | geometry: { 183 | type: "LineString", 184 | coordinates: [ 185 | [1, 0], 186 | [1, 1], 187 | ], 188 | }, 189 | }, 190 | ], 191 | }; 192 | 193 | const pathfinder = new PathFinder(network, { 194 | weight: function (a, b) { 195 | return { 196 | forward: distance(point(a), point(b)), 197 | }; 198 | }, 199 | }); 200 | const path1 = pathfinder.findPath(point([0, 0]), point([1, 1])); 201 | 202 | expect(path1).toBeTruthy(); 203 | expect(path1.path).toBeTruthy(); 204 | expect(path1.weight).toBeGreaterThan(0); 205 | 206 | const path2 = pathfinder.findPath(point([1, 1]), point([0, 0])); 207 | expect(path2).toBeUndefined(); 208 | }); 209 | 210 | test("can reduce data on edges", () => { 211 | const pathfinder = new PathFinder(geojson, { 212 | edgeDataReducer: function (a, p) { 213 | return { id: p.id }; 214 | }, 215 | edgeDataSeed: () => -1, 216 | }), 217 | path = pathfinder.findPath( 218 | point([8.44460166, 59.48947469]), 219 | point([8.44651, 59.513920000000006]) 220 | ); 221 | 222 | expect(path).toBeTruthy(); 223 | expect(path.edgeDatas).toBeTruthy(); 224 | expect( 225 | path.edgeDatas.every(function (e) { 226 | return e; 227 | }) 228 | ).toBeTruthy(); 229 | }); 230 | 231 | function edgeReduce(a, p) { 232 | const a_arr = a.id; 233 | p.id.forEach(function (id) { 234 | a_arr.push(id); 235 | }); 236 | return { id: Array.from(new Set(a_arr)) }; 237 | } 238 | 239 | test("captures all edge data", () => { 240 | const pathfinder = new PathFinder(geojson, { 241 | edgeDataReducer: edgeReduce, 242 | edgeDataSeed: (properties) => ({ id: [properties.id] }), 243 | }), 244 | path = pathfinder.findPath( 245 | point([8.44460166, 59.48947469]), 246 | point([8.44651, 59.513920000000006]) 247 | ); 248 | 249 | expect(path).toBeTruthy(); 250 | expect(path.edgeDatas).toBeTruthy(); 251 | expect( 252 | path.edgeDatas.some(function (e) { 253 | return e.id.indexOf(2001) > -1; 254 | }) 255 | ).toBeTruthy(); 256 | }); 257 | 258 | test("finding a path between nodes not in original graph", () => { 259 | const pathfinder = new PathFinder(geojson, { 260 | edgeDataReducer: function (a, p) { 261 | return { id: p.id }; 262 | }, 263 | edgeDataSeed: (properties) => ({ id: properties.id }), 264 | }), 265 | path = pathfinder.findPath(point([8.3, 59.3]), point([8.5, 59.6])); 266 | 267 | expect(path).toBeUndefined(); 268 | }); 269 | 270 | test("can route through large, complex one-way network", () => { 271 | const pathfinder = new PathFinder(largeNetwork, { 272 | weight: osmWeight, 273 | tolerance: 1e-9, 274 | }); 275 | const path = pathfinder.findPath( 276 | point([11.9954516, 57.7125743]), 277 | point([11.9608099, 57.6808616]) 278 | ); 279 | expect(path).toBeTruthy(); 280 | expect(path.path).toBeTruthy(); 281 | expect(path.weight).toBeGreaterThan(0); 282 | }); 283 | -------------------------------------------------------------------------------- /test/preprocessor.spec.js: -------------------------------------------------------------------------------- 1 | import largeNetwork from "./large-network.json"; 2 | import preprocess from "../src/preprocessor"; 3 | import { expect, test } from "vitest"; 4 | import osmWeight from "./osm-weight"; 5 | import twoIslands from "./two-islands.json"; 6 | import createTopology from "../src/topology"; 7 | 8 | test("preprocesses a large network", () => { 9 | var highwaySpeeds = { 10 | motorway: 110, 11 | trunk: 90, 12 | primary: 80, 13 | secondary: 70, 14 | tertiary: 50, 15 | unclassified: 50, 16 | road: 50, 17 | residential: 30, 18 | service: 30, 19 | living_street: 20, 20 | }; 21 | 22 | var unknowns = {}; 23 | 24 | preprocess(largeNetwork, { weight: osmWeight, precision: 1e-9 }); 25 | }); 26 | 27 | test("compacts islands correctly", () => { 28 | const graph = preprocess(twoIslands); 29 | expect(Object.keys(graph.compactedVertices).length).toEqual(2); 30 | }); 31 | -------------------------------------------------------------------------------- /test/topologySpec.js: -------------------------------------------------------------------------------- 1 | import topology from "../dist/esm/topology"; 2 | import geojson from "./network.json"; 3 | import { test } from "tape"; 4 | 5 | test("can create topology", function (t) { 6 | var topo = topology(geojson); 7 | t.ok(topo); 8 | t.ok(topo.vertices); 9 | t.ok(topo.edges); 10 | 11 | t.equal(Object.keys(topo.vertices).length, 889); 12 | 13 | t.end(); 14 | }); 15 | -------------------------------------------------------------------------------- /test/two-islands.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "name": "two-islands", 4 | "crs": { 5 | "type": "name", 6 | "properties": { 7 | "name": "urn:ogc:def:crs:OGC:1.3:CRS84" 8 | } 9 | }, 10 | "features": [ 11 | { 12 | "type": "Feature", 13 | "properties": {}, 14 | "geometry": { 15 | "type": "LineString", 16 | "coordinates": [ 17 | [ 18 | 13.576004, 19 | 56.221499 20 | ], 21 | [ 22 | 13.607143, 23 | 56.230412 24 | ], 25 | [ 26 | 13.633767, 27 | 56.199621 28 | ], 29 | [ 30 | 13.60367, 31 | 56.187119 32 | ], 33 | [ 34 | 13.564197, 35 | 56.211197 36 | ], 37 | [ 38 | 13.576004, 39 | 56.221499 40 | ] 41 | ] 42 | } 43 | }, 44 | { 45 | "type": "Feature", 46 | "properties": {}, 47 | "geometry": { 48 | "type": "LineString", 49 | "coordinates": [ 50 | [ 51 | 13.579477, 52 | 56.180289 53 | ], 54 | [ 55 | 13.626474, 56 | 56.177164 57 | ], 58 | [ 59 | 13.621497, 60 | 56.145794 61 | ], 62 | [ 63 | 13.577394, 64 | 56.142784 65 | ], 66 | [ 67 | 13.560609, 68 | 56.173228 69 | ], 70 | [ 71 | 13.579477, 72 | 56.180289 73 | ] 74 | ] 75 | } 76 | } 77 | ] 78 | } -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["node"], 5 | "module": "CommonJS", 6 | "strict": true, 7 | "outDir": "dist/cjs", 8 | "sourceMap": true, 9 | "declaration": true 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules", "**/*.spec.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["node"], 5 | "module": "ES6", 6 | "strict": true, 7 | "outDir": "dist/esm", 8 | "sourceMap": true, 9 | "declaration": true 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules", "**/*.spec.ts"] 13 | } 14 | --------------------------------------------------------------------------------