terra-route 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -8
- package/dist/graph/graph.d.ts +83 -0
- package/dist/graph/methods/connected.d.ts +9 -0
- package/dist/graph/methods/nodes.d.ts +17 -0
- package/dist/graph/methods/unique-segments.d.ts +5 -0
- package/dist/terra-route.cjs +1 -1
- package/dist/terra-route.cjs.map +1 -1
- package/dist/terra-route.d.ts +15 -27
- package/dist/terra-route.modern.js +1 -1
- package/dist/terra-route.modern.js.map +1 -1
- package/dist/terra-route.module.js +1 -1
- package/dist/terra-route.module.js.map +1 -1
- package/dist/terra-route.umd.js +1 -1
- package/dist/terra-route.umd.js.map +1 -1
- package/dist/test-utils/utils.d.ts +50 -0
- package/jest.config.js +1 -0
- package/package.json +7 -3
- package/src/data/network-5-cc.geojson +822 -0
- package/src/data/network.geojson +21910 -820
- package/src/distance/haversine.ts +1 -0
- package/src/graph/graph.ts +151 -0
- package/src/graph/methods/connected.spec.ts +217 -0
- package/src/graph/methods/connected.ts +168 -0
- package/src/graph/methods/nodes.spec.ts +317 -0
- package/src/graph/methods/nodes.ts +77 -0
- package/src/graph/methods/unique-segments.spec.ts +16 -0
- package/src/graph/methods/unique-segments.ts +69 -0
- package/src/heap/min-heap.ts +50 -46
- package/src/terra-route.compare.spec.ts +3 -1
- package/src/terra-route.spec.ts +12 -1
- package/src/terra-route.ts +122 -99
- package/src/test-utils/create.ts +39 -0
- package/src/test-utils/{test-utils.ts → generate-network.ts} +9 -188
- package/src/test-utils/utils.ts +228 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Terra Route
|
|
2
2
|
|
|
3
|
-
Terra Route aims to be a fast library for routing on GeoJSON LineStrings networks, where LineStrings share identical coordinates. Terra Routes main aim is currently performance - it uses
|
|
3
|
+
Terra Route aims to be a fast library for routing on GeoJSON LineStrings networks, where LineStrings share identical coordinates. Terra Routes main aim is currently performance - it uses A* to help achieve this.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -81,6 +81,23 @@ const route = router.getRoute(startPoint, endPoint);
|
|
|
81
81
|
console.log("Shortest route:", JSON.stringify(route, null, 2));
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
## Additional Functionality
|
|
85
|
+
|
|
86
|
+
Terra Route also exposes functionality for understanding GeoJSON route networks better called `LineStringGraph`. This can be useful for debugging as this class has a series of methods for determining things like unique nodes, edges, connected components as well as all their counts. With this class you can understand your graph better programmatically, for example determining it's size and if it is correctly connected.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
const graph = new LineStringGraph(network);
|
|
90
|
+
|
|
91
|
+
// Return all nodes in the graph as FeatureCollection<Point>, where each unique node is a Feature<Point>
|
|
92
|
+
const graphPoints = graph.getNodes();
|
|
93
|
+
|
|
94
|
+
// Return all the unique edges as FeatureCollection<LineString>, where each unique edge is a Feature<LineString>
|
|
95
|
+
const graphEdges = graph.getEdges();
|
|
96
|
+
|
|
97
|
+
// The longest possible shortest path in the graph between two nodes (i.e. graph diameter)
|
|
98
|
+
const longestShortestPath = graph.getMaxLengthShortestPath()
|
|
99
|
+
```
|
|
100
|
+
|
|
84
101
|
## Benchmarks
|
|
85
102
|
|
|
86
103
|
The benchmarks make use of a series out route example route networks from OSM in a moderate sized section of East London. It runs against the GeoJSON Path Finder library and also the ngraph.graph library. You can run the benchmarks yourself using:
|
|
@@ -92,19 +109,19 @@ npm run benchmark
|
|
|
92
109
|
Here is an example output of a benchmark run for routing:
|
|
93
110
|
|
|
94
111
|
<pre>
|
|
95
|
-
Terra Route
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
ngraph.graph | ██████████████████████████████████████████████████ 1587ms
|
|
112
|
+
Terra Route | ███████████ 270ms
|
|
113
|
+
GeoJSON Path Finder | █████████████████████████ 591ms
|
|
114
|
+
ngraph.graph | ██████████████████████████████████████████████████ 1177ms
|
|
99
115
|
</pre>
|
|
100
116
|
|
|
101
|
-
Using default Haversine distance, Terra Route is approximately
|
|
117
|
+
Using default Haversine distance, Terra Route is approximately 2x faster than GeoJSON Path Finder with Haversine distance for A -> B path finding. If you pass in the CheapRuler distance metric (you can use the exposed `createCheapRuler` function), it is approximately x5 faster than GeoJSON Path Finder with Haversine distance.
|
|
102
118
|
|
|
103
|
-
For initialisation of the network, Terra Route is
|
|
119
|
+
For initialisation of the network, Terra Route is approximately 10x faster with Haversine than GeoJSON Path Finder. Terra Draw splits out instantiating the Class of the library from the actual graph building, which is done via `buildRouteGraph`. This allows you to defer graph creation to an appropriate time.
|
|
104
120
|
|
|
105
121
|
## Limitations
|
|
106
122
|
|
|
107
|
-
Terra Route does not currently support weighting functions.
|
|
123
|
+
- Terra Route does not currently support weighting functions.
|
|
124
|
+
- Coordinates must be identical to be considered connected
|
|
108
125
|
|
|
109
126
|
## Acknowledgements
|
|
110
127
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Feature, FeatureCollection, LineString, Point } from "geojson";
|
|
2
|
+
/**
|
|
3
|
+
* Represents a graph constructed from a GeoJSON FeatureCollection of LineString features.
|
|
4
|
+
* This class provides methods to analyze the graph, including connected components, node and edge counts,
|
|
5
|
+
* and shortest paths. Coordinates in the LineStrings are considered connected if they share identical coordinates.
|
|
6
|
+
*/
|
|
7
|
+
export declare class LineStringGraph {
|
|
8
|
+
constructor(network: FeatureCollection<LineString>);
|
|
9
|
+
private network;
|
|
10
|
+
/**
|
|
11
|
+
* Sets the network for the graph.
|
|
12
|
+
* This method replaces the current network with a new one.
|
|
13
|
+
* @param network A GeoJSON FeatureCollection of LineString features representing the network.
|
|
14
|
+
*/
|
|
15
|
+
setNetwork(network: FeatureCollection<LineString>): void;
|
|
16
|
+
/**
|
|
17
|
+
* Gets the current network of the graph.
|
|
18
|
+
* @returns A GeoJSON FeatureCollection of LineString features representing the network.
|
|
19
|
+
*/
|
|
20
|
+
getNetwork(): FeatureCollection<LineString>;
|
|
21
|
+
/**
|
|
22
|
+
* Gets the connected components of the graph.
|
|
23
|
+
* @returns An array of FeatureCollection<LineString> representing the connected components.
|
|
24
|
+
*/
|
|
25
|
+
getConnectedComponents(): FeatureCollection<LineString>[];
|
|
26
|
+
/**
|
|
27
|
+
* Gets the count of connected components in the graph.
|
|
28
|
+
* @returns The number of connected components in the graph.
|
|
29
|
+
*/
|
|
30
|
+
getConnectedComponentCount(): number;
|
|
31
|
+
/**
|
|
32
|
+
* Gets the count of unique nodes and edges in the graph.
|
|
33
|
+
* @returns An object containing the counts of nodes and edges.
|
|
34
|
+
*/
|
|
35
|
+
getNodeAndEdgeCount(): {
|
|
36
|
+
nodeCount: number;
|
|
37
|
+
edgeCount: number;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Gets the unique nodes of the graph as a FeatureCollection of Point features.
|
|
41
|
+
* @returns A FeatureCollection<Point> containing the nodes of the graph.
|
|
42
|
+
*/
|
|
43
|
+
getNodes(): FeatureCollection<Point>;
|
|
44
|
+
/**
|
|
45
|
+
* Gets the count of unique nodes in the graph.
|
|
46
|
+
* @returns The number of unique nodes in the graph.
|
|
47
|
+
*/
|
|
48
|
+
getNodeCount(): number;
|
|
49
|
+
/**
|
|
50
|
+
* Gets the unique edges of the graph as a FeatureCollection of LineString features. Each edge is represented as a LineString.
|
|
51
|
+
* This method ensures that each edge is unique, meaning that edges are not duplicated in the collection. Each linestring only
|
|
52
|
+
* two coordinates, representing the start and end points of the edge.
|
|
53
|
+
* @returns A FeatureCollection<LineString> containing the unique edges of the graph.
|
|
54
|
+
*/
|
|
55
|
+
getEdges(): FeatureCollection<LineString>;
|
|
56
|
+
/**
|
|
57
|
+
* Gets the length of the longest edge in the graph based on the length of the LineString.
|
|
58
|
+
* If no edges exist, it returns -1.
|
|
59
|
+
* @returns The length of the longest edge in meters, or 0 if no edges exist.
|
|
60
|
+
*/
|
|
61
|
+
getLongestEdgeLength(): number;
|
|
62
|
+
/**
|
|
63
|
+
* Gets the length of the shortest edge in the graph based on the length of the LineString.
|
|
64
|
+
* If no edges exist, it returns -1.
|
|
65
|
+
* @returns The length of the shortest edge in meters, or 0 if no edges exist.
|
|
66
|
+
*/
|
|
67
|
+
getShortestEdgeLength(): number;
|
|
68
|
+
/**
|
|
69
|
+
* Gets the longest edge in the graph based on the length of the LineString.
|
|
70
|
+
* @returns The longest edge as a Feature<LineString> or null if no edges exist.
|
|
71
|
+
*/
|
|
72
|
+
getLongestEdge(): Feature<LineString> | null;
|
|
73
|
+
/**
|
|
74
|
+
* Gets the shortest edge in the graph based on the length of the LineString.
|
|
75
|
+
* @returns The shortest edge as a Feature<LineString> or null if no edges exist.
|
|
76
|
+
*/
|
|
77
|
+
getShortestEdge(): Feature<LineString> | null;
|
|
78
|
+
/**
|
|
79
|
+
* Gets the count of unique edges in the graph.
|
|
80
|
+
* @returns The number of unique edges in the graph.
|
|
81
|
+
*/
|
|
82
|
+
getEdgeCount(): number;
|
|
83
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { FeatureCollection, LineString } from 'geojson';
|
|
2
|
+
/**
|
|
3
|
+
* Counts the number of connected components in a graph represented by LineString features in a GeoJSON FeatureCollection.
|
|
4
|
+
* Each LineString is treated as an edge in the graph, and connected components are determined by shared coordinates.
|
|
5
|
+
* @param featureCollection - A GeoJSON FeatureCollection containing LineString features
|
|
6
|
+
* @returns The number of connected components in the graph represented by the LineStrings
|
|
7
|
+
*/
|
|
8
|
+
export declare function graphGetConnectedComponentCount(featureCollection: FeatureCollection<LineString>): number;
|
|
9
|
+
export declare function graphGetConnectedComponents(featureCollection: FeatureCollection<LineString>): FeatureCollection<LineString>[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Feature, FeatureCollection, LineString, Point } from 'geojson';
|
|
2
|
+
/**
|
|
3
|
+
* Counts the unique nodes and edges in a GeoJSON FeatureCollection of LineString features.
|
|
4
|
+
* @param featureCollection - A GeoJSON FeatureCollection containing LineString features
|
|
5
|
+
* @returns An object containing the count of unique nodes and edges
|
|
6
|
+
*/
|
|
7
|
+
export declare function graphGetNodeAndEdgeCount(featureCollection: FeatureCollection<LineString>): {
|
|
8
|
+
nodeCount: number;
|
|
9
|
+
edgeCount: number;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Converts a FeatureCollection of LineString features into a FeatureCollection of Point features,
|
|
13
|
+
* where each unique coordinate in the LineStrings becomes a Point.
|
|
14
|
+
* @param lines - A GeoJSON FeatureCollection containing LineString features
|
|
15
|
+
* @returns A FeatureCollection of Point features representing unique nodes
|
|
16
|
+
*/
|
|
17
|
+
export declare function graphGetNodesAsPoints(lines: FeatureCollection<LineString>): Feature<Point>[];
|
package/dist/terra-route.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function
|
|
1
|
+
function e(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=Array(t);r<t;r++)n[r]=e[r];return n}function t(t,r){var n="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(n)return(n=n.call(t)).next.bind(n);if(Array.isArray(t)||(n=function(t,r){if(t){if("string"==typeof t)return e(t,r);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?e(t,r):void 0}}(t))||r&&t&&"number"==typeof t.length){n&&(t=n);var o=0;return function(){return o>=t.length?{done:!0}:{done:!1,value:t[o++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var r=function(e,t){var r=function(e){return e*Math.PI/180},n=r(e[1]),o=r(e[0]),a=r(t[1]),i=a-n,s=r(t[0])-o,u=Math.sin(i/2)*Math.sin(i/2)+Math.cos(n)*Math.cos(a)*Math.sin(s/2)*Math.sin(s/2);return 2*Math.atan2(Math.sqrt(u),Math.sqrt(1-u))*6371e3/1e3},n=/*#__PURE__*/function(){function e(){this.heap=[],this.insertCounter=0}var t=e.prototype;return t.insert=function(e,t){var r={key:e,value:t,index:this.insertCounter++},n=this.heap.length;for(this.heap.push(r);n>0;){var o=n-1>>>1,a=this.heap[o];if(e>a.key||e===a.key&&r.index>a.index)break;this.heap[n]=a,n=o}this.heap[n]=r},t.extractMin=function(){var e=this.heap,t=e.length;if(0===t)return null;var r=e[0],n=e.pop();return t>1&&(e[0]=n,this.bubbleDown(0)),r.value},t.size=function(){return this.heap.length},t.bubbleDown=function(e){for(var t=this.heap,r=t.length,n=t[e],o=n.key,a=n.index;;){var i=1+(e<<1);if(i>=r)break;var s=i,u=t[i],h=i+1;if(h<r){var d=t[h];(d.key<u.key||d.key===u.key&&d.index<u.index)&&(s=h,u=d)}if(!(u.key<o||u.key===o&&u.index<a))break;t[e]=u,e=s}t[e]=n},e}();function o(e,r,n){n[e]=!0;for(var a,i=t(r[e]);!(a=i()).done;){var s=a.value;n[s]||o(s,r,n)}}function a(e,t){var r=function(e,t){var r=e[0],n=t[0];return r<n||r===n&&e[1]<=t[1]?[e,t]:[t,e]}(e,t);return JSON.stringify([r[0],r[1]])}function i(e){for(var t=e.geometry.coordinates,n=0,o=0;o<t.length-1;o++)n+=r(t[o],t[o+1]);return n}var s=/*#__PURE__*/function(){function e(e){this.network=void 0,this.network=e}var r=e.prototype;return r.setNetwork=function(e){this.network=e},r.getNetwork=function(){return this.network},r.getConnectedComponents=function(){return function(e){var r=e.features,n=new Map,o=new Map;function a(e){return e[0]+","+e[1]}for(var i=0;i<r.length;i++)for(var s,u=t(r[i].geometry.coordinates);!(s=u()).done;){var h=a(s.value);o.has(h)||o.set(h,new Set),o.get(h).add(i)}for(var d=0;d<r.length;d++){n.set(d,new Set);for(var f,c=t(r[d].geometry.coordinates);!(f=c()).done;){var v=a(f.value),g=o.get(v);if(g)for(var l,p=t(g);!(l=p()).done;){var y=l.value;y!==d&&n.get(d).add(y)}}}var w=new Set,M=[];function m(e,o){for(var a=[e];a.length>0;){var i=a.pop();if(!w.has(i)){w.add(i),o.push(r[i]);var s=n.get(i);if(s)for(var u,h=t(s);!(u=h()).done;){var d=u.value;w.has(d)||a.push(d)}}}}for(var k=0;k<r.length;k++)if(!w.has(k)){var C=[];m(k,C),M.push({type:"FeatureCollection",features:C})}return M.sort(function(e,t){return e.features.length-t.features.length}),M}(this.network)},r.getConnectedComponentCount=function(){return function(e){for(var r,n=e.features,a=n.length,i=new Map,s=0;s<a;s++)for(var u,h=t(n[s].geometry.coordinates);!(u=h()).done;){var d=(r=u.value)[0]+","+r[1];i.has(d)||i.set(d,[]),i.get(d).push(s)}for(var f,c=Array.from({length:a},function(){return[]}),v=t(i.values());!(f=v()).done;)for(var g=f.value,l=0;l<g.length;l++)for(var p=l+1;p<g.length;p++){var y=g[l],w=g[p];c[y].push(w),c[w].push(y)}for(var M=new Array(a).fill(!1),m=0,k=0;k<a;k++)M[k]||(o(k,c,M),m++);return m}(this.network)},r.getNodeAndEdgeCount=function(){return function(e){for(var r,n=new Set,o=new Set,a=t(e.features);!(r=a()).done;){for(var i,s=r.value.geometry.coordinates,u=t(s);!(i=u()).done;)n.add(JSON.stringify(i.value));for(var h=0;h<s.length-1;h++){var d=(f=s[h+1],void 0,void 0,(c=JSON.stringify(s[h]))<(v=JSON.stringify(f))?c+"|"+v:v+"|"+c);o.add(d)}}var f,c,v;return{nodeCount:n.size,edgeCount:o.size}}(this.network)},r.getNodes=function(){return{type:"FeatureCollection",features:function(e){for(var r,n=new Set,o=[],a=t(e.features);!(r=a()).done;)for(var i,s=t(r.value.geometry.coordinates);!(i=s()).done;){var u=i.value,h=u.join(",");n.has(h)||(n.add(h),o.push({type:"Feature",geometry:{type:"Point",coordinates:u},properties:{}}))}return o}(this.network)}},r.getNodeCount=function(){return this.getNodeAndEdgeCount().nodeCount},r.getEdges=function(){return function(e){for(var r,n=new Map,o=t(e.features);!(r=o()).done;)for(var i=r.value.geometry.coordinates,s=0;s<i.length-1;s++){var u=i[s],h=i[s+1],d=a(u,h);n.has(d)||n.set(d,{type:"Feature",geometry:{type:"LineString",coordinates:[u,h]},properties:{}})}return{type:"FeatureCollection",features:Array.from(n.values())}}(this.network)},r.getLongestEdgeLength=function(){var e=this.getLongestEdge();return e?i(e):-1},r.getShortestEdgeLength=function(){var e=this.getShortestEdge();return e?i(e):-1},r.getLongestEdge=function(){var e=this.getEdges().features;if(0===e.length)return null;var t=e.sort(function(e,t){return i(e)-i(t)});return t[t.length-1]},r.getShortestEdge=function(){var e=this.getEdges().features;return 0===e.length?null:e.sort(function(e,t){return i(e)-i(t)})[0]},r.getEdgeCount=function(){return this.getNodeAndEdgeCount().edgeCount},e}(),u=/*#__PURE__*/function(){function e(e){var t,o;this.network=null,this.distanceMeasurement=void 0,this.heapConstructor=void 0,this.coordinateIndexMap=new Map,this.coordinates=[],this.adjacencyList=[],this.distanceMeasurement=null!=(t=null==e?void 0:e.distanceMeasurement)?t:r,this.heapConstructor=null!=(o=null==e?void 0:e.heap)?o:n}var o=e.prototype;return o.buildRouteGraph=function(e){this.network=e,this.coordinateIndexMap=new Map,this.coordinates=[],this.adjacencyList=[];for(var r,n=this.coordinateIndexMap,o=this.coordinates,a=this.adjacencyList,i=this.distanceMeasurement,s=t(e.features);!(r=s()).done;)for(var u=r.value.geometry.coordinates,h=0;h<u.length-1;h++){var d=u[h],f=d[0],c=d[1],v=u[h+1],g=v[0],l=v[1],p=n.get(f);p||(p=new Map,n.set(f,p));var y=p.get(c);void 0===y&&(y=o.length,o.push(u[h]),p.set(c,y),a[y]=[]);var w=n.get(g);w||(w=new Map,n.set(g,w));var M=w.get(l);void 0===M&&(M=o.length,o.push(u[h+1]),w.set(l,M),a[M]=[]);var m=i(u[h],u[h+1]);a[y].push({node:M,distance:m}),a[M].push({node:y,distance:m})}},o.getRoute=function(e,r){if(!this.network)throw new Error("Network not built. Please call buildRouteGraph(network) first.");var n=this.getOrCreateIndex(e.geometry.coordinates),o=this.getOrCreateIndex(r.geometry.coordinates);if(n===o)return null;var a=new this.heapConstructor;a.insert(0,n);var i=this.coordinates.length,s=new Array(i).fill(Infinity),u=new Array(i).fill(-1),h=new Array(i).fill(!1);for(s[n]=0;a.size()>0;){var d=a.extractMin();if(!h[d]){if(d===o)break;h[d]=!0;for(var f,c=t(this.adjacencyList[d]||[]);!(f=c()).done;){var v=f.value,g=s[d]+v.distance;if(g<s[v.node]){s[v.node]=g,u[v.node]=d;var l=this.distanceMeasurement(this.coordinates[v.node],this.coordinates[o]);a.insert(g+l,v.node)}}}}if(u[o]<0)return null;for(var p=[],y=o;y!==n;)p.unshift(this.coordinates[y]),y=u[y];return p.unshift(this.coordinates[n]),{type:"Feature",geometry:{type:"LineString",coordinates:p},properties:{}}},o.getOrCreateIndex=function(e){var t=e[0],r=e[1],n=this.coordinateIndexMap.get(t);n||(n=new Map,this.coordinateIndexMap.set(t,n));var o=n.get(r);return void 0===o&&(o=this.coordinates.length,this.coordinates.push(e),n.set(r,o),this.adjacencyList[o]=[]),o},e}();exports.LineStringGraph=s,exports.TerraRoute=u,exports.createCheapRuler=function(e){var t=1/298.257223563,r=t*(2-t),n=Math.PI/180,o=Math.cos(e*n),a=1/(1-r*(1-o*o)),i=Math.sqrt(a),s=6378.137*n,u=s*i*o,h=s*i*a*(1-r);return function(e,t){for(var r=e[0]-t[0];r<-180;)r+=360;for(;r>180;)r-=360;var n=r*u,o=(e[1]-t[1])*h;return Math.sqrt(n*n+o*o)}},exports.haversineDistance=r;
|
|
2
2
|
//# sourceMappingURL=terra-route.cjs.map
|
package/dist/terra-route.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"terra-route.cjs","sources":["../src/distance/haversine.ts","../src/heap/min-heap.ts","../src/terra-route.ts","../src/distance/cheap-ruler.ts"],"sourcesContent":["import { Position } from \"geojson\";\n\n/** Distance measured in kilometers */\nexport const haversineDistance = (pointOne: Position, pointTwo: Position): number => {\n const toRadians = (latOrLng: number) => (latOrLng * Math.PI) / 180;\n\n const phiOne = toRadians(pointOne[1]);\n const lambdaOne = toRadians(pointOne[0]);\n const phiTwo = toRadians(pointTwo[1]);\n const lambdaTwo = toRadians(pointTwo[0]);\n const deltaPhi = phiTwo - phiOne;\n const deltalambda = lambdaTwo - lambdaOne;\n\n const a =\n Math.sin(deltaPhi / 2) * Math.sin(deltaPhi / 2) +\n Math.cos(phiOne) *\n Math.cos(phiTwo) *\n Math.sin(deltalambda / 2) *\n Math.sin(deltalambda / 2);\n const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n\n const radius = 6371e3;\n const distance = radius * c;\n\n return distance / 1000;\n}\n","import { Heap } from \"./heap\";\n\nexport class MinHeap implements Heap {\n private heap: Array<{ key: number; value: number; index: number }> = [];\n private insertCounter = 0;\n\n insert(key: number, value: number): void {\n const node = { key, value, index: this.insertCounter++ };\n let idx = this.heap.length;\n this.heap.push(node);\n\n // Optimized Bubble Up\n while (idx > 0) {\n const parentIdx = (idx - 1) >>> 1; // Fast Math.floor((idx - 1) / 2)\n const parent = this.heap[parentIdx];\n if (node.key > parent.key || (node.key === parent.key && node.index > parent.index)) break;\n this.heap[idx] = parent;\n idx = parentIdx;\n }\n this.heap[idx] = node;\n }\n\n extractMin(): number | null {\n const length = this.heap.length;\n if (length === 0) return null;\n\n const minNode = this.heap[0];\n const endNode = this.heap.pop()!;\n\n if (length > 1) {\n this.heap[0] = endNode;\n this.bubbleDown(0);\n }\n\n return minNode.value;\n }\n\n size(): number {\n return this.heap.length;\n }\n\n private bubbleDown(idx: number): void {\n const { heap } = this;\n const length = heap.length;\n // Grab the parent node once, then move it down only if needed\n const node = heap[idx];\n const nodeKey = node.key;\n const nodeIndex = node.index;\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n // Calculate left and right child indexes\n const leftIdx = (idx << 1) + 1;\n if (leftIdx >= length) {\n // No children => we’re already in place\n break;\n }\n\n // Assume left child is the smaller one by default\n let smallestIdx = leftIdx;\n let smallestKey = heap[leftIdx].key;\n let smallestIndex = heap[leftIdx].index;\n\n const rightIdx = leftIdx + 1;\n if (rightIdx < length) {\n // Compare left child vs. right child\n const rightKey = heap[rightIdx].key;\n const rightIndex = heap[rightIdx].index;\n if (rightKey < smallestKey || (rightKey === smallestKey && rightIndex < smallestIndex)) {\n smallestIdx = rightIdx;\n smallestKey = rightKey;\n smallestIndex = rightIndex;\n }\n }\n\n // Compare the smaller child with the parent\n if (smallestKey < nodeKey || (smallestKey === nodeKey && smallestIndex < nodeIndex)) {\n // Swap the smaller child up\n heap[idx] = heap[smallestIdx];\n idx = smallestIdx;\n } else {\n // We’re in the correct position now, so stop\n break;\n }\n }\n\n // Place the original node in its final position\n heap[idx] = node;\n }\n}","import { FeatureCollection, LineString, Point, Feature, Position } from \"geojson\";\nimport { haversineDistance } from \"./distance/haversine\";\nimport { createCheapRuler } from \"./distance/cheap-ruler\";\nimport { MinHeap } from \"./heap/min-heap\";\nimport { HeapConstructor } from \"./heap/heap\";\n\n/**\n * TerraRoute is a routing utility for finding the shortest path\n * between two geographic points over a given GeoJSON LineString network.\n *\n * The class builds an internal graph structure based on the provided network,\n * then applies A* algorithm to compute the shortest route.\n */\nclass TerraRoute {\n private network: FeatureCollection<LineString> | undefined;\n private distanceMeasurement: (positionA: Position, positionB: Position) => number;\n private adjacencyList: Map<number, Array<{ node: number; distance: number }>> = new Map();\n private coords: Position[] = []\n private coordMap: Map<number, Map<number, number>> = new Map();\n private heap: HeapConstructor;\n\n /**\n * Creates a new instance of TerraRoute.\n * \n * @param distanceMeasurement - Optional custom distance measurement function (defaults to haversine distance).\n */\n constructor(options?: {\n distanceMeasurement?: (positionA: Position, positionB: Position) => number,\n heap?: HeapConstructor\n }) {\n this.heap = options?.heap ? options.heap : MinHeap\n this.distanceMeasurement = options?.distanceMeasurement ? options.distanceMeasurement : haversineDistance;\n }\n\n /**\n * Converts a coordinate into a unique index. If the coordinate already exists, returns its index.\n * Otherwise, assigns a new index and stores the coordinate.\n * \n * @param coord - A GeoJSON Position array representing [longitude, latitude].\n * @returns A unique numeric index for the coordinate.\n */\n private coordinateIndex(coord: Position): number {\n const [lng, lat] = coord;\n if (!this.coordMap.has(lng)) this.coordMap.set(lng, new Map());\n\n const latMap = this.coordMap.get(lng)!;\n if (latMap.has(lat)) {\n return latMap.get(lat)!;\n }\n\n const index = this.coords.length;\n this.coords.push(coord);\n latMap.set(lat, index);\n\n return index;\n }\n\n /**\n * Builds the internal graph representation (adjacency list) from the input network.\n * Each LineString segment is translated into graph edges with associated distances.\n * Assumes that the network is a connected graph of LineStrings with shared coordinates. Calling this \n * method with a new network overwrite any existing network and reset all internal data structures.\n * \n * @param network - A GeoJSON FeatureCollection of LineStrings representing the road network.\n */\n public buildRouteGraph(network: FeatureCollection<LineString>): void {\n this.network = network;\n this.adjacencyList = new Map();\n this.coords = [];\n this.coordMap = new Map();\n\n for (const feature of this.network.features) {\n const coords = feature.geometry.coordinates;\n for (let i = 0; i < coords.length - 1; i++) {\n const aIndex = this.coordinateIndex(coords[i]);\n const bIndex = this.coordinateIndex(coords[i + 1]);\n const distance = this.distanceMeasurement(coords[i], coords[i + 1]);\n\n if (!this.adjacencyList.has(aIndex)) this.adjacencyList.set(aIndex, []);\n if (!this.adjacencyList.has(bIndex)) this.adjacencyList.set(bIndex, []);\n\n this.adjacencyList.get(aIndex)!.push({ node: bIndex, distance });\n this.adjacencyList.get(bIndex)!.push({ node: aIndex, distance });\n }\n }\n }\n\n /**\n * Computes the shortest route between two points in the network using the A* algorithm.\n * \n * @param start - A GeoJSON Point Feature representing the start location.\n * @param end - A GeoJSON Point Feature representing the end location.\n * @returns A GeoJSON LineString Feature representing the shortest path, or null if no path is found.\n * \n * @throws Error if the network has not been built yet with buildRouteGraph(network).\n */\n public getRoute(\n start: Feature<Point>,\n end: Feature<Point>\n ): Feature<LineString> | null {\n if (!this.network) {\n throw new Error(\"Network not built. Please call buildRouteGraph(network) first.\");\n }\n\n const startIndex = this.coordinateIndex(start.geometry.coordinates);\n const endIndex = this.coordinateIndex(end.geometry.coordinates);\n\n if (startIndex === endIndex) {\n return null;\n }\n\n const openSet = new this.heap();\n openSet.insert(0, startIndex);\n\n const cameFrom = new Map<number, number>();\n const gScore = new Map<number, number>([[startIndex, 0]]);\n const visited = new Set<number>();\n\n while (openSet.size() > 0) {\n // Extract the node with the smallest fScore\n const current = openSet.extractMin()!;\n\n // If we've reached the end node, we're done\n if (current === endIndex) {\n break;\n }\n\n visited.add(current);\n\n // Explore neighbors\n for (const neighbor of this.adjacencyList.get(current) || []) {\n // Tentative cost from start to this neighbor\n const tentativeG = (gScore.get(current) ?? Infinity) + neighbor.distance;\n\n // If this path to neighbor is better, record it\n if (tentativeG < (gScore.get(neighbor.node) ?? Infinity)) {\n cameFrom.set(neighbor.node, current);\n gScore.set(neighbor.node, tentativeG);\n\n // Calculate fScore: gScore + heuristic distance to the end\n const fScore =\n tentativeG +\n this.distanceMeasurement(this.coords[neighbor.node], this.coords[endIndex]);\n\n openSet.insert(fScore, neighbor.node);\n }\n }\n }\n\n // If we never set a path to the end node, there's no route\n if (!cameFrom.has(endIndex)) {\n return null;\n }\n\n // Reconstruct the path from end node to start node\n const path: Position[] = [];\n let node = endIndex;\n\n while (node !== undefined) {\n path.unshift(this.coords[node]);\n node = cameFrom.get(node)!;\n }\n\n return {\n type: \"Feature\",\n geometry: { type: \"LineString\", coordinates: path },\n properties: {},\n };\n }\n\n}\n\nexport { TerraRoute, createCheapRuler, haversineDistance }","import { Position } from \"geojson\";\n\n// This code is based on Mapbox's cheap-ruler library:\n\n// ISC License\n\n// Copyright (c) 2024, Mapbox\n\n// Permission to use, copy, modify, and/or distribute this software for any purpose\n// with or without fee is hereby granted, provided that the above copyright notice\n// and this permission notice appear in all copies.\n\n// THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\n// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND\n// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\n// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS\n// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER\n// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF\n// THIS SOFTWARE.\n\n/**\n * Creates a function for fast geodesic distance approximation using local scaling constants\n * based on a reference latitude. Useful for city-scale distances.\n *\n * @param {number} lat - Reference latitude in degrees\n * @returns {(a: Position, b: Position) => number} - Function that computes distance between two points\n * \n * @example\n * const distance = createCheapRuler(50.5);\n * const d = distance([30.5, 50.5], [30.51, 50.49]);\n * \n */\nexport function createCheapRuler(lat: number): (a: Position, b: Position) => number {\n const RE = 6378.137; // Earth's equatorial radius in kilometers\n const FE = 1 / 298.257223563; // Earth's flattening\n const E2 = FE * (2 - FE);\n const RAD = Math.PI / 180;\n\n const cosLat = Math.cos(lat * RAD);\n const w2 = 1 / (1 - E2 * (1 - cosLat * cosLat));\n const w = Math.sqrt(w2);\n\n const m = RAD * RE;\n const kx = m * w * cosLat; // scale for longitude\n const ky = m * w * w2 * (1 - E2); // scale for latitude\n\n return function distance(a: Position, b: Position): number {\n let deltaLng = a[0] - b[0];\n\n while (deltaLng < -180) deltaLng += 360;\n while (deltaLng > 180) deltaLng -= 360;\n\n const dx = deltaLng * kx;\n const dy = (a[1] - b[1]) * ky;\n\n return Math.sqrt(dx * dx + dy * dy);\n };\n}"],"names":["haversineDistance","pointOne","pointTwo","toRadians","latOrLng","Math","PI","phiOne","lambdaOne","phiTwo","deltaPhi","deltalambda","a","sin","cos","atan2","sqrt","MinHeap","this","heap","insertCounter","_proto","prototype","insert","key","value","node","index","idx","length","push","parentIdx","parent","extractMin","minNode","endNode","pop","bubbleDown","size","nodeKey","nodeIndex","leftIdx","smallestIdx","smallestKey","smallestIndex","rightIdx","rightKey","rightIndex","TerraRoute","options","network","distanceMeasurement","adjacencyList","Map","coords","coordMap","coordinateIndex","coord","lng","lat","has","set","latMap","get","buildRouteGraph","_step","_iterator","_createForOfIteratorHelperLoose","features","done","geometry","coordinates","i","aIndex","bIndex","distance","getRoute","start","end","Error","startIndex","endIndex","openSet","cameFrom","gScore","visited","Set","current","add","_step2","_iterator2","_gScore$get","_gScore$get2","neighbor","tentativeG","Infinity","fScore","path","undefined","unshift","type","properties","FE","E2","RAD","cosLat","w2","w","m","kx","ky","b","deltaLng","dx","dy"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"terra-route.cjs","sources":["../src/distance/haversine.ts","../src/heap/min-heap.ts","../src/graph/methods/connected.ts","../src/graph/methods/unique-segments.ts","../src/test-utils/utils.ts","../src/graph/graph.ts","../src/graph/methods/nodes.ts","../src/terra-route.ts","../src/distance/cheap-ruler.ts"],"sourcesContent":["import { Position } from \"geojson\";\n\n/** Distance measured in kilometers */\nexport const haversineDistance = (pointOne: Position, pointTwo: Position): number => {\n const toRadians = (latOrLng: number) => (latOrLng * Math.PI) / 180;\n\n const phiOne = toRadians(pointOne[1]);\n const lambdaOne = toRadians(pointOne[0]);\n const phiTwo = toRadians(pointTwo[1]);\n const lambdaTwo = toRadians(pointTwo[0]);\n const deltaPhi = phiTwo - phiOne;\n const deltalambda = lambdaTwo - lambdaOne;\n\n const a =\n Math.sin(deltaPhi / 2) * Math.sin(deltaPhi / 2) +\n Math.cos(phiOne) *\n Math.cos(phiTwo) *\n Math.sin(deltalambda / 2) *\n Math.sin(deltalambda / 2);\n const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n\n const radius = 6371e3;\n const distance = radius * c;\n\n\n return distance / 1000;\n}\n","import { Heap } from \"./heap\";\n\nexport class MinHeap implements Heap {\n private heap: Array<{ key: number; value: number; index: number }> = [];\n private insertCounter = 0;\n\n insert(key: number, value: number): void {\n const node = { key, value, index: this.insertCounter++ };\n let currentIndex = this.heap.length;\n this.heap.push(node);\n\n while (currentIndex > 0) {\n const parentIndex = (currentIndex - 1) >>> 1;\n const parent = this.heap[parentIndex];\n\n if (\n key > parent.key ||\n (key === parent.key && node.index > parent.index)\n ) {\n break;\n }\n\n this.heap[currentIndex] = parent;\n currentIndex = parentIndex;\n }\n\n this.heap[currentIndex] = node;\n }\n\n extractMin(): number | null {\n const heap = this.heap;\n const length = heap.length;\n\n if (length === 0) {\n return null;\n }\n\n const minNode = heap[0];\n const endNode = heap.pop()!;\n\n if (length > 1) {\n heap[0] = endNode;\n this.bubbleDown(0);\n }\n\n return minNode.value;\n }\n\n size(): number {\n return this.heap.length;\n }\n\n private bubbleDown(index: number): void {\n const heap = this.heap;\n const length = heap.length;\n const node = heap[index];\n const nodeKey = node.key;\n const nodeIndex = node.index;\n\n while (true) {\n const leftChildIndex = (index << 1) + 1;\n if (leftChildIndex >= length) {\n break;\n }\n\n let smallestIndex = leftChildIndex;\n let smallest = heap[leftChildIndex];\n\n const rightChildIndex = leftChildIndex + 1;\n if (rightChildIndex < length) {\n const right = heap[rightChildIndex];\n if (\n right.key < smallest.key ||\n (right.key === smallest.key && right.index < smallest.index)\n ) {\n smallestIndex = rightChildIndex;\n smallest = right;\n }\n }\n\n if (\n smallest.key < nodeKey ||\n (smallest.key === nodeKey && smallest.index < nodeIndex)\n ) {\n heap[index] = smallest;\n index = smallestIndex;\n } else {\n break;\n }\n }\n\n heap[index] = node;\n }\n}\n","import { Feature, FeatureCollection, LineString, Position } from 'geojson'\n\n/**\n * Counts the number of connected components in a graph represented by LineString features in a GeoJSON FeatureCollection.\n * Each LineString is treated as an edge in the graph, and connected components are determined by shared coordinates.\n * @param featureCollection - A GeoJSON FeatureCollection containing LineString features\n * @returns The number of connected components in the graph represented by the LineStrings\n */\nexport function graphGetConnectedComponentCount(\n featureCollection: FeatureCollection<LineString>\n): number {\n const features = featureCollection.features\n const numberOfFeatures = features.length\n\n // Map coordinates to feature indices\n const coordinateToFeatureIndices = new Map<string, number[]>()\n\n for (let index = 0; index < numberOfFeatures; index++) {\n const coordinates = features[index].geometry.coordinates\n\n for (const coordinate of coordinates) {\n const key = coordinateKey(coordinate)\n\n if (!coordinateToFeatureIndices.has(key)) {\n coordinateToFeatureIndices.set(key, [])\n }\n\n coordinateToFeatureIndices.get(key)!.push(index)\n }\n }\n\n // Build adjacency list for the graph\n const adjacencyList: number[][] = Array.from({ length: numberOfFeatures }, () => [])\n\n for (const indices of coordinateToFeatureIndices.values()) {\n for (let i = 0; i < indices.length; i++) {\n for (let j = i + 1; j < indices.length; j++) {\n const a = indices[i]\n const b = indices[j]\n adjacencyList[a].push(b)\n adjacencyList[b].push(a)\n }\n }\n }\n\n const visited = new Array<boolean>(numberOfFeatures).fill(false)\n let connectedComponents = 0\n\n for (let index = 0; index < numberOfFeatures; index++) {\n if (!visited[index]) {\n dfs(index, adjacencyList, visited)\n connectedComponents++\n }\n }\n\n return connectedComponents\n}\n\n/**\n * Depth-first search to mark all reachable nodes from the given index.\n * @param index - The current node index to start DFS from.\n * @param adjacencyList - The adjacency list representing the graph.\n * @param visited - An array to keep track of visited nodes.\n */\nfunction dfs(index: number, adjacencyList: number[][], visited: boolean[]): void {\n visited[index] = true\n\n for (const neighbor of adjacencyList[index]) {\n if (!visited[neighbor]) {\n dfs(neighbor, adjacencyList, visited)\n }\n }\n}\n\nfunction coordinateKey(position: Position): string {\n return `${position[0]},${position[1]}`\n}\n\nexport function graphGetConnectedComponents(\n featureCollection: FeatureCollection<LineString>\n): FeatureCollection<LineString>[] {\n const features = featureCollection.features\n const graph: Map<number, Set<number>> = new Map()\n const coordinateMap: Map<string, Set<number>> = new Map()\n\n function coordinateKey(coordinate: Position): string {\n return `${coordinate[0]},${coordinate[1]}`\n }\n\n // Build coordinate map: coordinate string -> Set of feature indices\n for (let index = 0; index < features.length; index++) {\n const coordinates = features[index].geometry.coordinates\n\n for (const coordinate of coordinates) {\n const key = coordinateKey(coordinate)\n\n if (!coordinateMap.has(key)) {\n coordinateMap.set(key, new Set())\n }\n\n coordinateMap.get(key)!.add(index)\n }\n }\n\n // Build adjacency list for graph\n for (let index = 0; index < features.length; index++) {\n graph.set(index, new Set())\n\n const coordinates = features[index].geometry.coordinates\n for (const coordinate of coordinates) {\n const key = coordinateKey(coordinate)\n const neighbors = coordinateMap.get(key)\n\n if (neighbors) {\n for (const neighborIndex of neighbors) {\n if (neighborIndex !== index) {\n graph.get(index)!.add(neighborIndex)\n }\n }\n }\n }\n }\n\n // DFS to find connected components\n const visited = new Set<number>()\n const components: FeatureCollection<LineString>[] = []\n\n function dfs(startIndex: number, currentComponent: Feature<LineString>[]): void {\n const stack: number[] = [startIndex]\n\n while (stack.length > 0) {\n const currentIndex = stack.pop()!\n\n if (visited.has(currentIndex)) {\n continue\n }\n\n visited.add(currentIndex)\n currentComponent.push(features[currentIndex])\n\n const neighbors = graph.get(currentIndex)\n if (neighbors) {\n for (const neighbor of neighbors) {\n if (!visited.has(neighbor)) {\n stack.push(neighbor)\n }\n }\n }\n }\n }\n\n for (let index = 0; index < features.length; index++) {\n if (!visited.has(index)) {\n const component: Feature<LineString>[] = []\n dfs(index, component)\n components.push({\n type: 'FeatureCollection',\n features: component\n })\n }\n }\n\n\n // Sort components by the number of features in ascending order\n components.sort((a, b) => a.features.length - b.features.length)\n\n return components\n}\n","import {\n Feature,\n FeatureCollection,\n LineString,\n Position\n} from 'geojson';\n\n/**\n * Normalize a segment so that [A, B] is equal to [B, A]\n */\nfunction normalizeSegment(start: Position, end: Position): [Position, Position] {\n const [aLat, aLng] = start;\n const [bLat, bLng] = end;\n\n if (\n aLat < bLat ||\n (aLat === bLat && aLng <= bLng)\n ) {\n return [start, end];\n }\n\n return [end, start];\n}\n\n/**\n * Convert a pair of Positions to a string key for deduplication\n */\nfunction segmentKey(start: Position, end: Position): string {\n const [normalizedStart, normalizedEnd] = normalizeSegment(start, end);\n return JSON.stringify([normalizedStart, normalizedEnd]);\n}\n\n/**\n * Breaks LineStrings in a FeatureCollection into unique single line segments\n */\nexport function graphGetUniqueSegments(\n input: FeatureCollection<LineString>\n): FeatureCollection<LineString> {\n const uniqueSegments = new Map<string, Feature<LineString>>();\n\n for (const feature of input.features) {\n const coordinates = feature.geometry.coordinates;\n\n for (let index = 0; index < coordinates.length - 1; index++) {\n const start = coordinates[index];\n const end = coordinates[index + 1];\n\n const key = segmentKey(start, end);\n\n if (!uniqueSegments.has(key)) {\n const segment: Feature<LineString> = {\n type: 'Feature',\n geometry: {\n type: 'LineString',\n coordinates: [start, end]\n },\n properties: {}\n };\n\n uniqueSegments.set(key, segment);\n }\n }\n }\n\n return {\n type: 'FeatureCollection',\n features: Array.from(uniqueSegments.values())\n };\n}\n","import { Position, Feature, Point, LineString, FeatureCollection } from \"geojson\";\nimport { haversineDistance } from \"../terra-route\";\n\n/**\n * Calculates the total length of a LineString route in meters.\n *\n * @param line - A GeoJSON Feature<LineString> representing the route\n * @returns The total length of the route in meters\n */\nexport function routeLength(\n line: Feature<LineString>,\n) {\n const lineCoords = line.geometry.coordinates;\n\n // Calculate the total route distance\n let routeDistance = 0;\n for (let i = 0; i < lineCoords.length - 1; i++) {\n routeDistance += haversineDistance(lineCoords[i], lineCoords[i + 1]);\n }\n return routeDistance\n}\n\n/**\n * Extracts unique coordinates from a FeatureCollection of LineStrings.\n *\n * @param collection - A GeoJSON FeatureCollection of LineStrings\n * @returns An array of unique Position coordinates\n */\nexport function getUniqueCoordinatesFromLineStrings(\n collection: FeatureCollection<LineString>\n): Position[] {\n const seen = new Set<string>();\n const unique: Position[] = [];\n\n for (const feature of collection.features) {\n if (feature.geometry.type !== \"LineString\") {\n continue;\n }\n\n for (const coord of feature.geometry.coordinates) {\n const key = `${coord[0]},${coord[1]}`;\n\n if (!seen.has(key)) {\n seen.add(key);\n unique.push(coord);\n }\n }\n }\n\n return unique;\n}\n\n/**\n * Validates a GeoJSON Feature<LineString> route.\n *\n * @param route - The GeoJSON feature to validate\n * @returns A boolean indicating if it is a valid LineString route\n */\nexport function getReasonIfLineStringInvalid(\n route: Feature<LineString> | null | undefined\n): string | undefined {\n // 1. Must exist\n if (!route) {\n return 'No feature';\n }\n\n // 2. Must be a Feature\n if (route.type !== \"Feature\") {\n return 'Not a Feature';\n }\n\n // 3. Must have a geometry of type LineString\n if (!route.geometry || route.geometry.type !== \"LineString\") {\n return 'Not a LineString';\n }\n\n // 4. Coordinates must be an array with length >= 2\n const coords = route.geometry.coordinates;\n if (!Array.isArray(coords) || coords.length < 2) {\n return `Not enough coordinates: ${coords.length} (${coords})`;\n }\n\n const seen = new Set<string>();\n\n // 5. Validate each coordinate is a valid Position\n // (At minimum, [number, number] or [number, number, number])\n for (const position of coords) {\n if (!Array.isArray(position)) {\n return 'Not a Position; not an array';\n }\n\n // Check numeric values, ignoring optional altitude\n if (\n position.length < 2 ||\n typeof position[0] !== \"number\" ||\n typeof position[1] !== \"number\"\n ) {\n return 'Not a Position; elements are not a numbers';\n }\n\n // 6. Check for duplicates\n const key = `${position[0]},${position[1]}`;\n if (seen.has(key)) {\n return `Duplicate coordinate: ${key}`;\n }\n seen.add(key);\n }\n}\n\n/**\n * Checks if the start and end coordinates of a LineString match the given start and end points.\n * \n * @param line - The LineString feature to check\n * @param start - The start point feature\n * @param end - The end point feature\n * @return True if the start and end coordinates match, false otherwise\n * */\nexport function startAndEndAreCorrect(line: Feature<LineString>, start: Feature<Point>, end: Feature<Point>): boolean {\n const lineCoords = line.geometry.coordinates;\n const startCoords = start.geometry.coordinates;\n const endCoords = end.geometry.coordinates;\n\n // Check if the first coordinate of the LineString matches the start point\n const startMatches = lineCoords[0][0] === startCoords[0] && lineCoords[0][1] === startCoords[1];\n\n // Check if the last coordinate of the LineString matches the end point\n const endMatches = lineCoords[lineCoords.length - 1][0] === endCoords[0] && lineCoords[lineCoords.length - 1][1] === endCoords[1];\n\n return startMatches && endMatches;\n}\n\n/**\n * Checks if the route represented by a LineString is longer than the direct path. \n * In theory, a route should always longer than the direct path if it has more than two points.\n * @param line - The LineString feature representing the route\n * @param start - The start point feature\n * @param end - The end point feature\n * @returns - True if the route is longer than the direct path, false otherwise\n */\nexport function routeIsLongerThanDirectPath(line: Feature<LineString>, start: Feature<Point>, end: Feature<Point>): boolean {\n const lineCoords = line.geometry.coordinates;\n const startCoords = start.geometry.coordinates;\n const endCoords = end.geometry.coordinates;\n\n if (lineCoords.length <= 2) {\n return true;\n }\n\n // Calculate the direct distance between the start and end points\n const directDistance = haversineDistance(startCoords, endCoords);\n\n // Calculate the route distance\n let routeDistance = 0;\n for (let i = 0; i < lineCoords.length - 1; i++) {\n routeDistance += haversineDistance(lineCoords[i], lineCoords[i + 1]);\n }\n\n // If the route distance is 0, it means the start and end points are the same\n if (routeDistance === 0) {\n return true;\n }\n\n if (routeDistance < directDistance) {\n\n // Check if the route distance is very close to the direct distance\n const absoluteDifference = Math.abs(routeDistance - directDistance);\n if (absoluteDifference < 0.000000000001) {\n return true;\n }\n\n return false;\n }\n\n return true\n}\n\n/**\n * Modifies a FeatureCollection of LineStrings to break connections\n * between lines that share coordinates, by adjusting one of the shared\n * coordinates within a given tolerance.\n * \n * @param collection - The input FeatureCollection of LineStrings\n * @param tolerance - The amount by which to offset shared coordinates (in degrees)\n * @returns A new FeatureCollection with modified coordinates\n */\nexport function disconnectLineStrings(\n collection: FeatureCollection<LineString>,\n tolerance: number\n): FeatureCollection<LineString> {\n const seenCoordinates = new Map<string, number>()\n\n function getCoordinateKey(coordinate: Position): string {\n return `${coordinate[0]},${coordinate[1]}`\n }\n\n function offsetCoordinate(coordinate: Position, count: number): Position {\n const offset = count * tolerance\n return [coordinate[0] + offset, coordinate[1] + offset]\n }\n\n const updatedFeatures = collection.features.map((feature) => {\n const updatedCoordinates: Position[] = feature.geometry.coordinates.map((coordinate) => {\n const key = getCoordinateKey(coordinate)\n\n if (seenCoordinates.has(key)) {\n const count = seenCoordinates.get(key)!\n seenCoordinates.set(key, count + 1)\n return offsetCoordinate(coordinate, count + 1)\n }\n\n seenCoordinates.set(key, 0)\n return coordinate\n })\n\n return {\n ...feature,\n geometry: {\n ...feature.geometry,\n coordinates: updatedCoordinates\n }\n }\n })\n\n return {\n ...collection,\n features: updatedFeatures\n }\n}\n","import { Feature, FeatureCollection, LineString, Point } from \"geojson\";\nimport { graphGetConnectedComponentCount, graphGetConnectedComponents } from \"./methods/connected\";\nimport { graphGetNodeAndEdgeCount, graphGetNodesAsPoints } from \"./methods/nodes\";\nimport { graphGetUniqueSegments } from \"./methods/unique-segments\";\nimport { routeLength } from \"../test-utils/utils\";\n\n/**\n * Represents a graph constructed from a GeoJSON FeatureCollection of LineString features.\n * This class provides methods to analyze the graph, including connected components, node and edge counts,\n * and shortest paths. Coordinates in the LineStrings are considered connected if they share identical coordinates.\n */\nexport class LineStringGraph {\n constructor(network: FeatureCollection<LineString>) {\n this.network = network;\n }\n\n private network: FeatureCollection<LineString>;\n\n /**\n * Sets the network for the graph.\n * This method replaces the current network with a new one.\n * @param network A GeoJSON FeatureCollection of LineString features representing the network.\n */\n setNetwork(network: FeatureCollection<LineString>) {\n this.network = network;\n }\n\n /**\n * Gets the current network of the graph.\n * @returns A GeoJSON FeatureCollection of LineString features representing the network.\n */\n getNetwork(): FeatureCollection<LineString> {\n return this.network;\n }\n\n /**\n * Gets the connected components of the graph.\n * @returns An array of FeatureCollection<LineString> representing the connected components.\n */\n getConnectedComponents(): FeatureCollection<LineString>[] {\n return graphGetConnectedComponents(this.network)\n }\n\n /**\n * Gets the count of connected components in the graph.\n * @returns The number of connected components in the graph.\n */\n getConnectedComponentCount(): number {\n return graphGetConnectedComponentCount(this.network);\n }\n\n /**\n * Gets the count of unique nodes and edges in the graph.\n * @returns An object containing the counts of nodes and edges.\n */\n getNodeAndEdgeCount(): { nodeCount: number, edgeCount: number } {\n return graphGetNodeAndEdgeCount(this.network);\n }\n\n /**\n * Gets the unique nodes of the graph as a FeatureCollection of Point features.\n * @returns A FeatureCollection<Point> containing the nodes of the graph.\n */\n getNodes(): FeatureCollection<Point> {\n const nodes = graphGetNodesAsPoints(this.network);\n return {\n type: \"FeatureCollection\",\n features: nodes\n };\n }\n\n /**\n * Gets the count of unique nodes in the graph.\n * @returns The number of unique nodes in the graph.\n */\n getNodeCount(): number {\n const { nodeCount } = this.getNodeAndEdgeCount();\n return nodeCount;\n }\n\n /**\n * Gets the unique edges of the graph as a FeatureCollection of LineString features. Each edge is represented as a LineString.\n * This method ensures that each edge is unique, meaning that edges are not duplicated in the collection. Each linestring only \n * two coordinates, representing the start and end points of the edge.\n * @returns A FeatureCollection<LineString> containing the unique edges of the graph.\n */\n getEdges(): FeatureCollection<LineString> {\n return graphGetUniqueSegments(this.network);\n }\n\n /**\n * Gets the length of the longest edge in the graph based on the length of the LineString.\n * If no edges exist, it returns -1.\n * @returns The length of the longest edge in meters, or 0 if no edges exist.\n */\n getLongestEdgeLength(): number {\n const longestEdge = this.getLongestEdge();\n if (!longestEdge) {\n return -1;\n }\n return routeLength(longestEdge);\n }\n\n /**\n * Gets the length of the shortest edge in the graph based on the length of the LineString.\n * If no edges exist, it returns -1.\n * @returns The length of the shortest edge in meters, or 0 if no edges exist.\n */\n getShortestEdgeLength(): number {\n const shortestEdge = this.getShortestEdge();\n if (!shortestEdge) {\n return -1;\n }\n return routeLength(shortestEdge);\n }\n\n /**\n * Gets the longest edge in the graph based on the length of the LineString.\n * @returns The longest edge as a Feature<LineString> or null if no edges exist.\n */\n getLongestEdge(): Feature<LineString> | null {\n const edges = this.getEdges().features;\n if (edges.length === 0) {\n return null;\n }\n const longestEdges = edges.sort((a, b) => routeLength(a) - routeLength(b));\n return longestEdges[longestEdges.length - 1];\n }\n\n /**\n * Gets the shortest edge in the graph based on the length of the LineString.\n * @returns The shortest edge as a Feature<LineString> or null if no edges exist.\n */\n getShortestEdge(): Feature<LineString> | null {\n const edges = this.getEdges().features;\n if (edges.length === 0) {\n return null;\n }\n const shortestEdges = edges.sort((a, b) => routeLength(a) - routeLength(b));\n return shortestEdges[0];\n }\n\n /**\n * Gets the count of unique edges in the graph.\n * @returns The number of unique edges in the graph.\n */\n getEdgeCount(): number {\n const { edgeCount } = this.getNodeAndEdgeCount();\n return edgeCount;\n }\n}\n","import { Feature, FeatureCollection, LineString, Point, Position } from 'geojson'\n\n/**\n * Counts the unique nodes and edges in a GeoJSON FeatureCollection of LineString features.\n * @param featureCollection - A GeoJSON FeatureCollection containing LineString features\n * @returns An object containing the count of unique nodes and edges\n */\nexport function graphGetNodeAndEdgeCount(\n featureCollection: FeatureCollection<LineString>\n): { nodeCount: number; edgeCount: number } {\n const nodeSet = new Set<string>()\n const edgeSet = new Set<string>()\n\n for (const feature of featureCollection.features) {\n const coordinates = feature.geometry.coordinates\n\n for (const coordinate of coordinates) {\n nodeSet.add(JSON.stringify(coordinate))\n }\n\n for (let i = 0; i < coordinates.length - 1; i++) {\n const coordinateOne = coordinates[i]\n const coordinateTwo = coordinates[i + 1]\n\n const edge = normalizeEdge(coordinateOne, coordinateTwo)\n edgeSet.add(edge)\n }\n }\n\n return {\n nodeCount: nodeSet.size,\n edgeCount: edgeSet.size,\n }\n}\n\nfunction normalizeEdge(coordinateOne: Position, coordinateTwo: Position): string {\n const stringOne = JSON.stringify(coordinateOne)\n const stringTwo = JSON.stringify(coordinateTwo)\n\n if (stringOne < stringTwo) {\n return `${stringOne}|${stringTwo}`\n }\n\n return `${stringTwo}|${stringOne}`\n}\n\n\n/**\n * Converts a FeatureCollection of LineString features into a FeatureCollection of Point features,\n * where each unique coordinate in the LineStrings becomes a Point.\n * @param lines - A GeoJSON FeatureCollection containing LineString features\n * @returns A FeatureCollection of Point features representing unique nodes\n */\nexport function graphGetNodesAsPoints(lines: FeatureCollection<LineString>): Feature<Point>[] {\n const seen = new Set<string>();\n const points: Feature<Point>[] = [];\n\n for (const feature of lines.features) {\n for (const coord of feature.geometry.coordinates) {\n const key = coord.join(',');\n\n if (!seen.has(key)) {\n seen.add(key);\n points.push({\n type: 'Feature',\n geometry: {\n type: 'Point',\n coordinates: coord\n },\n properties: {}\n });\n }\n }\n }\n\n return points;\n}\n","import { FeatureCollection, LineString, Point, Feature, Position } from \"geojson\";\nimport { haversineDistance } from \"./distance/haversine\";\nimport { createCheapRuler } from \"./distance/cheap-ruler\";\nimport { MinHeap } from \"./heap/min-heap\";\nimport { HeapConstructor } from \"./heap/heap\";\nimport { LineStringGraph } from \"./graph/graph\";\n\ninterface Router {\n buildRouteGraph(network: FeatureCollection<LineString>): void;\n getRoute(start: Feature<Point>, end: Feature<Point>): Feature<LineString> | null;\n}\n\nclass TerraRoute implements Router {\n private network: FeatureCollection<LineString> | null = null;\n private distanceMeasurement: (a: Position, b: Position) => number;\n private heapConstructor: HeapConstructor;\n\n // Map from longitude → (map from latitude → index)\n private coordinateIndexMap: Map<number, Map<number, number>> = new Map();\n private coordinates: Position[] = [];\n private adjacencyList: Array<Array<{ node: number; distance: number }>> = [];\n\n constructor(options?: {\n distanceMeasurement?: (a: Position, b: Position) => number;\n heap?: HeapConstructor;\n }) {\n this.distanceMeasurement = options?.distanceMeasurement ?? haversineDistance;\n this.heapConstructor = options?.heap ?? MinHeap;\n }\n\n /**\n * Converts a coordinate into a unique index. If the coordinate already exists, returns its index.\n * Otherwise, assigns a new index and stores the coordinate.\n * \n * @param coord - A GeoJSON Position array representing [longitude, latitude].\n * @returns A unique numeric index for the coordinate.\n */\n public buildRouteGraph(network: FeatureCollection<LineString>): void {\n this.network = network;\n\n // Reset everything\n this.coordinateIndexMap = new Map();\n this.coordinates = [];\n this.adjacencyList = [];\n\n // Hoist to locals for speed\n const coordIndexMapLocal = this.coordinateIndexMap;\n const coordsLocal = this.coordinates;\n const adjListLocal = this.adjacencyList;\n const measureDistance = this.distanceMeasurement;\n\n for (const feature of network.features) {\n const lineCoords = feature.geometry.coordinates;\n\n for (let i = 0; i < lineCoords.length - 1; i++) {\n const [lngA, latA] = lineCoords[i];\n const [lngB, latB] = lineCoords[i + 1];\n\n // get or assign index for A \n let latMapA = coordIndexMapLocal.get(lngA);\n if (!latMapA) {\n latMapA = new Map<number, number>();\n coordIndexMapLocal.set(lngA, latMapA);\n }\n let indexA = latMapA.get(latA);\n if (indexA === undefined) {\n indexA = coordsLocal.length;\n coordsLocal.push(lineCoords[i]);\n latMapA.set(latA, indexA);\n adjListLocal[indexA] = [];\n }\n\n // get or assign index for B \n let latMapB = coordIndexMapLocal.get(lngB);\n if (!latMapB) {\n latMapB = new Map<number, number>();\n coordIndexMapLocal.set(lngB, latMapB);\n }\n let indexB = latMapB.get(latB);\n if (indexB === undefined) {\n indexB = coordsLocal.length;\n coordsLocal.push(lineCoords[i + 1]);\n latMapB.set(latB, indexB);\n adjListLocal[indexB] = [];\n }\n\n // record the bidirectional edge \n const segmentDistance = measureDistance(lineCoords[i], lineCoords[i + 1]);\n adjListLocal[indexA].push({ node: indexB, distance: segmentDistance });\n adjListLocal[indexB].push({ node: indexA, distance: segmentDistance });\n }\n }\n }\n\n /**\n * Computes the shortest route between two points in the network using the A* algorithm.\n * \n * @param start - A GeoJSON Point Feature representing the start location.\n * @param end - A GeoJSON Point Feature representing the end location.\n * @returns A GeoJSON LineString Feature representing the shortest path, or null if no path is found.\n * \n * @throws Error if the network has not been built yet with buildRouteGraph(network).\n */\n public getRoute(\n start: Feature<Point>,\n end: Feature<Point>\n ): Feature<LineString> | null {\n if (!this.network) {\n throw new Error(\"Network not built. Please call buildRouteGraph(network) first.\");\n }\n\n // ensure start/end are in the index maps\n const startIndex = this.getOrCreateIndex(start.geometry.coordinates);\n const endIndex = this.getOrCreateIndex(end.geometry.coordinates);\n\n if (startIndex === endIndex) {\n return null;\n }\n\n const openSet = new this.heapConstructor();\n openSet.insert(0, startIndex);\n\n const nodeCount = this.coordinates.length;\n const gScore = new Array<number>(nodeCount).fill(Infinity);\n const cameFrom = new Array<number>(nodeCount).fill(-1);\n const visited = new Array<boolean>(nodeCount).fill(false);\n\n gScore[startIndex] = 0;\n\n while (openSet.size() > 0) {\n const current = openSet.extractMin()!;\n if (visited[current]) {\n continue;\n }\n if (current === endIndex) {\n break;\n }\n visited[current] = true;\n\n for (const neighbor of this.adjacencyList[current] || []) {\n const tentativeG = gScore[current] + neighbor.distance;\n if (tentativeG < gScore[neighbor.node]) {\n gScore[neighbor.node] = tentativeG;\n cameFrom[neighbor.node] = current;\n const heuristic = this.distanceMeasurement(\n this.coordinates[neighbor.node],\n this.coordinates[endIndex]\n );\n openSet.insert(tentativeG + heuristic, neighbor.node);\n }\n }\n }\n\n if (cameFrom[endIndex] < 0) {\n return null;\n }\n\n // Reconstruct path\n const path: Position[] = [];\n let current = endIndex;\n while (current !== startIndex) {\n path.unshift(this.coordinates[current]);\n current = cameFrom[current];\n }\n path.unshift(this.coordinates[startIndex]);\n\n return {\n type: \"Feature\",\n geometry: { type: \"LineString\", coordinates: path },\n properties: {},\n };\n }\n\n /**\n * Helper to index start/end in getRoute.\n */\n private getOrCreateIndex(coord: Position): number {\n const [lng, lat] = coord;\n let latMap = this.coordinateIndexMap.get(lng);\n if (!latMap) {\n latMap = new Map<number, number>();\n this.coordinateIndexMap.set(lng, latMap);\n }\n let index = latMap.get(lat);\n if (index === undefined) {\n index = this.coordinates.length;\n this.coordinates.push(coord);\n latMap.set(lat, index);\n // ensure adjacencyList covers this new node\n this.adjacencyList[index] = [];\n }\n return index;\n }\n}\n\nexport { TerraRoute, createCheapRuler, haversineDistance, LineStringGraph }","import { Position } from \"geojson\";\n\n// This code is based on Mapbox's cheap-ruler library:\n\n// ISC License\n\n// Copyright (c) 2024, Mapbox\n\n// Permission to use, copy, modify, and/or distribute this software for any purpose\n// with or without fee is hereby granted, provided that the above copyright notice\n// and this permission notice appear in all copies.\n\n// THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\n// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND\n// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\n// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS\n// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER\n// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF\n// THIS SOFTWARE.\n\n/**\n * Creates a function for fast geodesic distance approximation using local scaling constants\n * based on a reference latitude. Useful for city-scale distances.\n *\n * @param {number} lat - Reference latitude in degrees\n * @returns {(a: Position, b: Position) => number} - Function that computes distance between two points\n * \n * @example\n * const distance = createCheapRuler(50.5);\n * const d = distance([30.5, 50.5], [30.51, 50.49]);\n * \n */\nexport function createCheapRuler(lat: number): (a: Position, b: Position) => number {\n const RE = 6378.137; // Earth's equatorial radius in kilometers\n const FE = 1 / 298.257223563; // Earth's flattening\n const E2 = FE * (2 - FE);\n const RAD = Math.PI / 180;\n\n const cosLat = Math.cos(lat * RAD);\n const w2 = 1 / (1 - E2 * (1 - cosLat * cosLat));\n const w = Math.sqrt(w2);\n\n const m = RAD * RE;\n const kx = m * w * cosLat; // scale for longitude\n const ky = m * w * w2 * (1 - E2); // scale for latitude\n\n return function distance(a: Position, b: Position): number {\n let deltaLng = a[0] - b[0];\n\n while (deltaLng < -180) deltaLng += 360;\n while (deltaLng > 180) deltaLng -= 360;\n\n const dx = deltaLng * kx;\n const dy = (a[1] - b[1]) * ky;\n\n return Math.sqrt(dx * dx + dy * dy);\n };\n}"],"names":["haversineDistance","pointOne","pointTwo","toRadians","latOrLng","Math","PI","phiOne","lambdaOne","phiTwo","deltaPhi","deltalambda","a","sin","cos","atan2","sqrt","MinHeap","heap","this","insertCounter","_proto","prototype","insert","key","value","node","index","currentIndex","length","push","parentIndex","parent","extractMin","minNode","endNode","pop","bubbleDown","size","nodeKey","nodeIndex","leftChildIndex","smallestIndex","smallest","rightChildIndex","right","dfs","adjacencyList","visited","_step3","_iterator3","_createForOfIteratorHelperLoose","done","neighbor","segmentKey","start","end","_normalizeSegment","aLat","bLat","normalizeSegment","JSON","stringify","routeLength","line","lineCoords","geometry","coordinates","routeDistance","i","LineStringGraph","network","setNetwork","getNetwork","getConnectedComponents","featureCollection","features","graph","Map","coordinateMap","coordinateKey","coordinate","_step4","_iterator4","has","set","Set","get","add","_step5","_iterator5","neighbors","_iterator6","_step6","neighborIndex","components","startIndex","currentComponent","stack","_step7","_iterator7","component","type","sort","b","graphGetConnectedComponents","getConnectedComponentCount","position","numberOfFeatures","coordinateToFeatureIndices","_step","_iterator","_step2","Array","from","_iterator2","values","indices","j","fill","connectedComponents","graphGetConnectedComponentCount","getNodeAndEdgeCount","nodeSet","edgeSet","edge","coordinateTwo","stringOne","stringTwo","nodeCount","edgeCount","graphGetNodeAndEdgeCount","getNodes","lines","seen","points","coord","join","properties","graphGetNodesAsPoints","getNodeCount","getEdges","input","uniqueSegments","graphGetUniqueSegments","getLongestEdgeLength","longestEdge","getLongestEdge","getShortestEdgeLength","shortestEdge","getShortestEdge","edges","longestEdges","getEdgeCount","TerraRoute","options","_options$distanceMeas","_options$heap","distanceMeasurement","heapConstructor","coordinateIndexMap","buildRouteGraph","coordIndexMapLocal","coordsLocal","adjListLocal","measureDistance","_lineCoords$i","lngA","latA","_lineCoords","lngB","latB","latMapA","indexA","undefined","latMapB","indexB","segmentDistance","distance","getRoute","Error","getOrCreateIndex","endIndex","openSet","gScore","Infinity","cameFrom","current","tentativeG","heuristic","path","unshift","lng","lat","latMap","FE","E2","RAD","cosLat","w2","w","m","kx","ky","deltaLng","dx","dy"],"mappings":"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"}
|
package/dist/terra-route.d.ts
CHANGED
|
@@ -2,27 +2,20 @@ import { FeatureCollection, LineString, Point, Feature, Position } from "geojson
|
|
|
2
2
|
import { haversineDistance } from "./distance/haversine";
|
|
3
3
|
import { createCheapRuler } from "./distance/cheap-ruler";
|
|
4
4
|
import { HeapConstructor } from "./heap/heap";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*/
|
|
12
|
-
declare class TerraRoute {
|
|
5
|
+
import { LineStringGraph } from "./graph/graph";
|
|
6
|
+
interface Router {
|
|
7
|
+
buildRouteGraph(network: FeatureCollection<LineString>): void;
|
|
8
|
+
getRoute(start: Feature<Point>, end: Feature<Point>): Feature<LineString> | null;
|
|
9
|
+
}
|
|
10
|
+
declare class TerraRoute implements Router {
|
|
13
11
|
private network;
|
|
14
12
|
private distanceMeasurement;
|
|
13
|
+
private heapConstructor;
|
|
14
|
+
private coordinateIndexMap;
|
|
15
|
+
private coordinates;
|
|
15
16
|
private adjacencyList;
|
|
16
|
-
private coords;
|
|
17
|
-
private coordMap;
|
|
18
|
-
private heap;
|
|
19
|
-
/**
|
|
20
|
-
* Creates a new instance of TerraRoute.
|
|
21
|
-
*
|
|
22
|
-
* @param distanceMeasurement - Optional custom distance measurement function (defaults to haversine distance).
|
|
23
|
-
*/
|
|
24
17
|
constructor(options?: {
|
|
25
|
-
distanceMeasurement?: (
|
|
18
|
+
distanceMeasurement?: (a: Position, b: Position) => number;
|
|
26
19
|
heap?: HeapConstructor;
|
|
27
20
|
});
|
|
28
21
|
/**
|
|
@@ -32,15 +25,6 @@ declare class TerraRoute {
|
|
|
32
25
|
* @param coord - A GeoJSON Position array representing [longitude, latitude].
|
|
33
26
|
* @returns A unique numeric index for the coordinate.
|
|
34
27
|
*/
|
|
35
|
-
private coordinateIndex;
|
|
36
|
-
/**
|
|
37
|
-
* Builds the internal graph representation (adjacency list) from the input network.
|
|
38
|
-
* Each LineString segment is translated into graph edges with associated distances.
|
|
39
|
-
* Assumes that the network is a connected graph of LineStrings with shared coordinates. Calling this
|
|
40
|
-
* method with a new network overwrite any existing network and reset all internal data structures.
|
|
41
|
-
*
|
|
42
|
-
* @param network - A GeoJSON FeatureCollection of LineStrings representing the road network.
|
|
43
|
-
*/
|
|
44
28
|
buildRouteGraph(network: FeatureCollection<LineString>): void;
|
|
45
29
|
/**
|
|
46
30
|
* Computes the shortest route between two points in the network using the A* algorithm.
|
|
@@ -52,5 +36,9 @@ declare class TerraRoute {
|
|
|
52
36
|
* @throws Error if the network has not been built yet with buildRouteGraph(network).
|
|
53
37
|
*/
|
|
54
38
|
getRoute(start: Feature<Point>, end: Feature<Point>): Feature<LineString> | null;
|
|
39
|
+
/**
|
|
40
|
+
* Helper to index start/end in getRoute.
|
|
41
|
+
*/
|
|
42
|
+
private getOrCreateIndex;
|
|
55
43
|
}
|
|
56
|
-
export { TerraRoute, createCheapRuler, haversineDistance };
|
|
44
|
+
export { TerraRoute, createCheapRuler, haversineDistance, LineStringGraph };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const t=(t,e)=>{const
|
|
1
|
+
const t=(t,e)=>{const n=t=>t*Math.PI/180,o=n(t[1]),s=n(t[0]),r=n(e[1]),i=r-o,c=n(e[0])-s,a=Math.sin(i/2)*Math.sin(i/2)+Math.cos(o)*Math.cos(r)*Math.sin(c/2)*Math.sin(c/2);return 2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a))*6371e3/1e3};function e(t){const e=1/298.257223563,n=e*(2-e),o=Math.PI/180,s=Math.cos(t*o),r=1/(1-n*(1-s*s)),i=Math.sqrt(r),c=6378.137*o,a=c*i*s,h=c*i*r*(1-n);return function(t,e){let n=t[0]-e[0];for(;n<-180;)n+=360;for(;n>180;)n-=360;const o=n*a,s=(t[1]-e[1])*h;return Math.sqrt(o*o+s*s)}}class n{constructor(){this.heap=[],this.insertCounter=0}insert(t,e){const n={key:t,value:e,index:this.insertCounter++};let o=this.heap.length;for(this.heap.push(n);o>0;){const e=o-1>>>1,s=this.heap[e];if(t>s.key||t===s.key&&n.index>s.index)break;this.heap[o]=s,o=e}this.heap[o]=n}extractMin(){const t=this.heap,e=t.length;if(0===e)return null;const n=t[0],o=t.pop();return e>1&&(t[0]=o,this.bubbleDown(0)),n.value}size(){return this.heap.length}bubbleDown(t){const e=this.heap,n=e.length,o=e[t],s=o.key,r=o.index;for(;;){const o=1+(t<<1);if(o>=n)break;let i=o,c=e[o];const a=o+1;if(a<n){const t=e[a];(t.key<c.key||t.key===c.key&&t.index<c.index)&&(i=a,c=t)}if(!(c.key<s||c.key===s&&c.index<r))break;e[t]=c,t=i}e[t]=o}}function o(t,e,n){n[t]=!0;for(const s of e[t])n[s]||o(s,e,n)}function s(t,e){const n=JSON.stringify(t),o=JSON.stringify(e);return n<o?`${n}|${o}`:`${o}|${n}`}function r(t,e){const[n,o]=function(t,e){const[n,o]=t,[s,r]=e;return n<s||n===s&&o<=r?[t,e]:[e,t]}(t,e);return JSON.stringify([n,o])}function i(e){const n=e.geometry.coordinates;let o=0;for(let e=0;e<n.length-1;e++)o+=t(n[e],n[e+1]);return o}class c{constructor(t){this.network=void 0,this.network=t}setNetwork(t){this.network=t}getNetwork(){return this.network}getConnectedComponents(){return function(t){const e=t.features,n=new Map,o=new Map;function s(t){return`${t[0]},${t[1]}`}for(let t=0;t<e.length;t++){const n=e[t].geometry.coordinates;for(const e of n){const n=s(e);o.has(n)||o.set(n,new Set),o.get(n).add(t)}}for(let t=0;t<e.length;t++){n.set(t,new Set);const r=e[t].geometry.coordinates;for(const e of r){const r=s(e),i=o.get(r);if(i)for(const e of i)e!==t&&n.get(t).add(e)}}const r=new Set,i=[];function c(t,o){const s=[t];for(;s.length>0;){const t=s.pop();if(r.has(t))continue;r.add(t),o.push(e[t]);const i=n.get(t);if(i)for(const t of i)r.has(t)||s.push(t)}}for(let t=0;t<e.length;t++)if(!r.has(t)){const e=[];c(t,e),i.push({type:"FeatureCollection",features:e})}return i.sort((t,e)=>t.features.length-e.features.length),i}(this.network)}getConnectedComponentCount(){return function(t){const e=t.features,n=e.length,s=new Map;for(let t=0;t<n;t++){const n=e[t].geometry.coordinates;for(const e of n){const n=`${(r=e)[0]},${r[1]}`;s.has(n)||s.set(n,[]),s.get(n).push(t)}}var r;const i=Array.from({length:n},()=>[]);for(const t of s.values())for(let e=0;e<t.length;e++)for(let n=e+1;n<t.length;n++){const o=t[e],s=t[n];i[o].push(s),i[s].push(o)}const c=new Array(n).fill(!1);let a=0;for(let t=0;t<n;t++)c[t]||(o(t,i,c),a++);return a}(this.network)}getNodeAndEdgeCount(){return function(t){const e=new Set,n=new Set;for(const o of t.features){const t=o.geometry.coordinates;for(const n of t)e.add(JSON.stringify(n));for(let e=0;e<t.length-1;e++){const o=s(t[e],t[e+1]);n.add(o)}}return{nodeCount:e.size,edgeCount:n.size}}(this.network)}getNodes(){return{type:"FeatureCollection",features:function(t){const e=new Set,n=[];for(const o of t.features)for(const t of o.geometry.coordinates){const o=t.join(",");e.has(o)||(e.add(o),n.push({type:"Feature",geometry:{type:"Point",coordinates:t},properties:{}}))}return n}(this.network)}}getNodeCount(){const{nodeCount:t}=this.getNodeAndEdgeCount();return t}getEdges(){return function(t){const e=new Map;for(const n of t.features){const t=n.geometry.coordinates;for(let n=0;n<t.length-1;n++){const o=t[n],s=t[n+1],i=r(o,s);e.has(i)||e.set(i,{type:"Feature",geometry:{type:"LineString",coordinates:[o,s]},properties:{}})}}return{type:"FeatureCollection",features:Array.from(e.values())}}(this.network)}getLongestEdgeLength(){const t=this.getLongestEdge();return t?i(t):-1}getShortestEdgeLength(){const t=this.getShortestEdge();return t?i(t):-1}getLongestEdge(){const t=this.getEdges().features;if(0===t.length)return null;const e=t.sort((t,e)=>i(t)-i(e));return e[e.length-1]}getShortestEdge(){const t=this.getEdges().features;return 0===t.length?null:t.sort((t,e)=>i(t)-i(e))[0]}getEdgeCount(){const{edgeCount:t}=this.getNodeAndEdgeCount();return t}}class a{constructor(e){var o,s;this.network=null,this.distanceMeasurement=void 0,this.heapConstructor=void 0,this.coordinateIndexMap=new Map,this.coordinates=[],this.adjacencyList=[],this.distanceMeasurement=null!=(o=null==e?void 0:e.distanceMeasurement)?o:t,this.heapConstructor=null!=(s=null==e?void 0:e.heap)?s:n}buildRouteGraph(t){this.network=t,this.coordinateIndexMap=new Map,this.coordinates=[],this.adjacencyList=[];const e=this.coordinateIndexMap,n=this.coordinates,o=this.adjacencyList,s=this.distanceMeasurement;for(const r of t.features){const t=r.geometry.coordinates;for(let r=0;r<t.length-1;r++){const[i,c]=t[r],[a,h]=t[r+1];let u=e.get(i);u||(u=new Map,e.set(i,u));let d=u.get(c);void 0===d&&(d=n.length,n.push(t[r]),u.set(c,d),o[d]=[]);let f=e.get(a);f||(f=new Map,e.set(a,f));let g=f.get(h);void 0===g&&(g=n.length,n.push(t[r+1]),f.set(h,g),o[g]=[]);const l=s(t[r],t[r+1]);o[d].push({node:g,distance:l}),o[g].push({node:d,distance:l})}}}getRoute(t,e){if(!this.network)throw new Error("Network not built. Please call buildRouteGraph(network) first.");const n=this.getOrCreateIndex(t.geometry.coordinates),o=this.getOrCreateIndex(e.geometry.coordinates);if(n===o)return null;const s=new this.heapConstructor;s.insert(0,n);const r=this.coordinates.length,i=new Array(r).fill(Infinity),c=new Array(r).fill(-1),a=new Array(r).fill(!1);for(i[n]=0;s.size()>0;){const t=s.extractMin();if(!a[t]){if(t===o)break;a[t]=!0;for(const e of this.adjacencyList[t]||[]){const n=i[t]+e.distance;if(n<i[e.node]){i[e.node]=n,c[e.node]=t;const r=this.distanceMeasurement(this.coordinates[e.node],this.coordinates[o]);s.insert(n+r,e.node)}}}}if(c[o]<0)return null;const h=[];let u=o;for(;u!==n;)h.unshift(this.coordinates[u]),u=c[u];return h.unshift(this.coordinates[n]),{type:"Feature",geometry:{type:"LineString",coordinates:h},properties:{}}}getOrCreateIndex(t){const[e,n]=t;let o=this.coordinateIndexMap.get(e);o||(o=new Map,this.coordinateIndexMap.set(e,o));let s=o.get(n);return void 0===s&&(s=this.coordinates.length,this.coordinates.push(t),o.set(n,s),this.adjacencyList[s]=[]),s}}export{c as LineStringGraph,a as TerraRoute,e as createCheapRuler,t as haversineDistance};
|
|
2
2
|
//# sourceMappingURL=terra-route.modern.js.map
|