Skip to content

Commit

Permalink
added(core): Introducing the ["findPath"](https://heremaps.github.io/…
Browse files Browse the repository at this point in the history
…xyz-maps/docs/classes/core.featureprovider.html#findpath) method for optimal client-side path finding on a GeoJSON road network supporting advanced options such as custom turn restrictions and weights.

Signed-off-by: Tim Deubler <[email protected]>
  • Loading branch information
TerminalTim committed Jan 18, 2024
1 parent ded5ccc commit 73929ae
Show file tree
Hide file tree
Showing 7 changed files with 559 additions and 9 deletions.
130 changes: 130 additions & 0 deletions packages/common/src/AStar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright (C) 2019-2023 HERE Europe B.V.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

import {BinaryHeap} from './BinaryHeap';
import {distance} from './geotools';

export type AStarNode = { point: number[], data?: any };

class Node implements AStarNode {
point: number[];
data?: any;
// "real" cost to reach the node (start->node)
g: number;
// approximate cost to reach the goal node (node->goal)
h: number;
// total cost (g+h)
f: number;
parent: Node | null;

constructor(point: number[], g: number, h: number, parent: Node | null = null) {
this.point = point;
this.g = g;
this.h = h;
this.f = g + h;
this.parent = parent;
}
}

export class AStar {
static precision = 1e5;
static calculateDistance(point1: number[], point2: number[]): number {
// const dx = point2[0] - point1[0];
// const dy = point2[1] - point1[1];
// return Math.sqrt(dx * dx + dy * dy);
return distance(point1, point2);
}

private static weight(nodeA: AStarNode, nodeB: AStarNode): number {
return AStar.calculateDistance(nodeA.point, nodeB.point);
}

static isPointEqual(point1: number[], point2: number[]): boolean {
const precision = AStar.precision;
return (point1[0] * precision ^ 0) === (point2[0] * precision ^ 0) && (point1[1] * precision ^ 0) === (point2[1] * precision ^ 0);
// return point1[0] === point2[0] && point1[1] === point2[1];
}

private static pointKey(node: AStarNode): number {
const precision = AStar.precision;
const point = node.point;
return (point[0] * precision ^ 0) * 1e7 + (point[1] * precision ^ 0);
}

public static findPath(
from: AStarNode,
endNode: AStarNode,
getNeighbors: (node: AStarNode) => AStarNode[],
weight: (nodeA: AStarNode, nodeB: AStarNode) => number = AStar.weight
): AStarNode[] | null {
const start = from.point;
const endCoordinate = endNode.point;
const openList = new BinaryHeap<Node>((a, b) => a.f - b.f);
const closedList = new Set<number>();
// const startNode = new NavNode(start, 0, AStar.calculateDistance(start, endCoordinate));
const startNode = new Node(start, 0, Infinity);
startNode.data = from.data;

openList.push(startNode);

while (openList.size() > 0) {
const currentNode = openList.pop()!;

if (AStar.isPointEqual(currentNode.point, endCoordinate)) {
// reconstruct the path
const path: AStarNode[] = [];
let current: Node | null = currentNode;
while (current !== null) {
path.unshift(current);
current = current.parent;
}
return path;
}

const pointKey = AStar.pointKey(currentNode);
closedList.add(pointKey);
for (const neighborNode of getNeighbors(currentNode)) {
const {point: neighbor, data} = neighborNode;
const neighborKey = AStar.pointKey(neighborNode);
if (closedList.has(neighborKey)) {
continue;
}
const g = currentNode.g + weight(currentNode, neighborNode);
const h = weight(neighborNode, endNode);
// const h = AStar.calculateDistance(neighborNode.point, endNode.point);
const existingNode = openList.find((node) => AStar.isPointEqual(node.point, neighbor));
if (existingNode) {
if (g < existingNode.g) {
existingNode.g = g;
existingNode.h = h;
existingNode.f = g + h;
existingNode.parent = currentNode;
existingNode.data = data;
}
} else {
const newNode = new Node(neighbor, g, h, currentNode);
newNode.data = data;
openList.push(newNode);
}
}
}
// no path found
return null;
}
}
100 changes: 100 additions & 0 deletions packages/common/src/BinaryHeap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright (C) 2019-2023 HERE Europe B.V.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/
export class BinaryHeap<T> {
private heap: T[];
private compare: (a: T, b: T) => number;

constructor(compare: (a: T, b: T) => number) {
this.heap = [];
this.compare = compare;
}

find(predicate: (this: void, value: T, index: number, obj: T[]) => boolean) {
return this.heap.find(predicate);
}

includes(value: T) {
return this.heap.includes(value);
}

push(value: T): void {
this.heap.push(value);
this.bubbleUp(this.heap.length - 1);
}

pop(): T | undefined {
const {heap} = this;

if (!heap.length) return;

const result = heap[0];
const end = heap.pop()!;
if (heap.length) {
heap[0] = end;
this.sinkDown(0);
}
return result;
}

size(): number {
return this.heap.length;
}

private bubbleUp(index: number): void {
const element = this.heap[index];
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
const parent = this.heap[parentIndex];
if (this.compare(element, parent) >= 0) break;
this.heap[parentIndex] = element;
this.heap[index] = parent;
index = parentIndex;
}
}

private sinkDown(index: number): void {
const length = this.heap.length;
const element = this.heap[index];
while (true) {
let leftChildIndex = 2 * index + 1;
let rightChildIndex = 2 * index + 2;
let swap = null;
let leftChild;
let rightChild;

if (leftChildIndex < length) {
leftChild = this.heap[leftChildIndex];
if (this.compare(leftChild, element) < 0) {
swap = leftChildIndex;
}
}
if (rightChildIndex < length) {
rightChild = this.heap[rightChildIndex];
if ((swap === null && this.compare(rightChild, element) < 0) ||
(swap !== null && this.compare(rightChild, leftChild!) < 0)) {
swap = rightChildIndex;
}
}
if (swap === null) break;
this.heap[index] = this.heap[swap];
this.heap[swap] = element;
index = swap;
}
}
}
6 changes: 4 additions & 2 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import Set from './Set';
import Map from './Map';
import Queue from './Queue';
import * as vec3 from './Vec3';
import {AStar, AStarNode} from './AStar';
import {BinaryHeap} from './BinaryHeap';

// make sure global ns is also available for webpack users.
let scp: any = global;
Expand All @@ -38,10 +40,10 @@ let scp: any = global;
// support for deprecated root namespace
(<any>global).HERE = (<any>global).here;

const common = {LRU, TaskManager, Listener, parseJSONArray, JSUtils, geotools, global, Queue, Set, Map, vec3, geometry};
const common = {AStar, BinaryHeap, LRU, TaskManager, Listener, parseJSONArray, JSUtils, geotools, global, Queue, Set, Map, vec3, geometry};

scp.common = common;

export {LRU, TaskManager, Task, TaskOptions, Listener, parseJSONArray, JSUtils, geotools, global, Queue, Set, Map, vec3, geometry};
export {AStar, AStarNode, BinaryHeap, LRU, TaskManager, Task, TaskOptions, Listener, parseJSONArray, JSUtils, geotools, global, Queue, Set, Map, vec3, geometry};

export default common;
8 changes: 5 additions & 3 deletions packages/core/src/features/Feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {GeoJSONFeature, GeoJSONBBox} from './GeoJSON';
/**
* represents a Feature in GeoJSON Feature format.
*/
export class Feature implements GeoJSONFeature {
export class Feature<GeometryType = string> implements GeoJSONFeature<GeometryType> {
/**
* id of the feature.
*/
Expand All @@ -33,7 +33,9 @@ export class Feature implements GeoJSONFeature {
/**
* The properties associated with the feature.
*/
properties: { [name: string]: any; } | null;
properties: {
[name: string]: any;
} | null;

/**
* The type of the feature is a string with 'Feature' as its value.
Expand Down Expand Up @@ -83,7 +85,7 @@ export class Feature implements GeoJSONFeature {
*```
*/
geometry: {
type: 'Point' | 'MultiPoint' | 'LineString' | 'MultiLineString' | 'Polygon' | 'MultiPolygon' | string,
type: 'Point' | 'MultiPoint' | 'LineString' | 'MultiLineString' | 'Polygon' | 'MultiPolygon' | GeometryType | string,
coordinates: any[]
/**
* cached polygon centroid
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/features/GeoJSON.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ export type GeoJSONCoordinate = number[]; // [number, number] | [number, number,
/**
* A GeoJSON Feature object.
*/
export interface GeoJSONFeature {
export interface GeoJSONFeature<GeometryType = string> {
/**
* id of the feature.
*/
id?: string | number;

/**
* Type of a GeoJSONFeature is 'Feature'
* Type of GeoJSONFeature is 'Feature'
*/
type: 'Feature' | string;

Expand Down Expand Up @@ -105,7 +105,7 @@ export interface GeoJSONFeature {
*```
*/
geometry: {
type: 'Point' | 'MultiPoint' | 'LineString' | 'MultiLineString' | 'Polygon' | 'MultiPolygon' | string,
type: 'Point' | 'MultiPoint' | 'LineString' | 'MultiLineString' | 'Polygon' | 'MultiPolygon' | GeometryType | string,
coordinates: GeoJSONCoordinate | GeoJSONCoordinate[] | GeoJSONCoordinate[][] | GeoJSONCoordinate[][][]
};
}
Expand All @@ -121,5 +121,5 @@ export interface GeoJSONFeatureCollection {
/**
* An array of {@link GeoJSONFeature | GeoJSONFeatures}.
*/
features: GeoJSONFeature[]
features: GeoJSONFeature[];
}
Loading

0 comments on commit 73929ae

Please sign in to comment.