-
Notifications
You must be signed in to change notification settings - Fork 20
/
Copy pathfindPaths.js
220 lines (179 loc) · 6.15 KB
/
findPaths.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
/**
* This module renders shortest paths on a city roads graph.
*
* Usage example:
*
* ``` js
* let city = await requireModule('https://cdn.jsdelivr.net/gh/anvaka/city-script/dist/city-script.js');
*
* city.findPaths(scene, {
* from: { lat: 47.8167059, lon: -122.3293886 }
* })
* ```
*/
let createRandom = require('ngraph.random')
let toGraph = require('./toGraph');
let npath = require('ngraph.path');
let distances = {
manhattan,
euclid,
projected,
canberra
}
module.exports = function findPaths(options) {
if (options === window.scene) {
console.warn('The (scene, options) API is deprecated, please read the new API here:')
console.warn('https://github.com/anvaka/city-script/blob/master/FindPaths.md')
throw new Error('Please read https://github.com/anvaka/city-script/blob/master/FindPaths.md')
}
options = options || {};
const scene = options.scene || window.scene;
// We will be using seed random number generator, so that results are predictable.
const random = createRandom(options.seed || 42);
// How many shortest paths do we want?
let count = options.pathCount || 2000;
// How exactly should we measure a distance between two nodes?
let distanceFunction = getDistanceFunction(options.distance);
// Helper data structures to perform graph algorithms.
let {graph, nodes, nodeIds, projector, mainLayer} = getGraphFromScene(scene);
// Should we search single source shortest paths, or just randomly sample graph?
let foundFromId = getSourceNodeId(options.from);
// Sometimes they want to keep the original city. Otherwise let's clear it
if (!options.keepScene) mainLayer.hide();
let linksCount = graph.getLinksCount();
let wgl = scene.getWGL();
const wglRenderer = scene.getRenderer()
const pathLimit = linksCount * 10;
let betweenLines = new wgl.WireCollection(pathLimit, {
allowColors: false,
is3D: false
});
betweenLines.id = 'paths';
updateLinesColorFromScene();
wglRenderer.appendChild(betweenLines);
let totalAdded = 0;
let explored = 0;
let pathFinder = npath.nba(graph, {
distance: distance,
heuristic: distance
});
// Timeout is better than animation frame, as this could be done even when
// tab is closed.
let handle = setTimeout(compute, 0);
// This is console API. Allows clients to dispose, or get access to the
// wire collection.
return {
dispose,
lines: betweenLines
}
function dispose() {
clearTimeout(handle);
scene.off('color-change', onColorChange);
}
function compute() {
let elapsedTime = 0;
let startTime = window.performance.now();
let timeLimit = 20;
while (elapsedTime < timeLimit && explored < count) {
let fromId = foundFromId || nodeIds[Math.floor(random.nextDouble() * nodeIds.length)];
let toId = nodeIds[Math.floor(random.nextDouble() * nodeIds.length)];
let found = pathFinder.find(fromId, toId).map(l => l.data);
for (let i = 1; i < found.length; ++i) {
betweenLines.add({from: projector(found[i - 1]), to: projector(found[i])});
}
totalAdded += found.length;
explored += 1;
if (explored % 50 === 0) {
console.info('Explored ' + explored + ' shortest paths.');
}
elapsedTime = (window.performance.now() - startTime);
}
if (totalAdded < pathLimit && explored < count) {
handle = setTimeout(compute, 0);
}
wglRenderer.renderFrame();
}
function distance(n1, n2) {
return distanceFunction(n1.data, n2.data);
}
function updateLinesColorFromScene() {
let color = scene.lineColor.toRgb()
betweenLines.color = {
r: color.r/255,
g: color.g/255,
b: color.b/255,
a: options.alpha || color.a // 0.05?
}
if (wglRenderer) wglRenderer.renderFrame();
}
function getSourceNodeId(nearOption) {
if (!nearOption) return; // they don't care. The algorithm runs for random pairs.
let lonLat = nearOption;
if(typeof lonLat === 'string') {
let parts = nearOption.split(',').map(x => parseFloat(x)).filter(x => Number.isFinite(x));
if (parts.length !== 2) {
throw new Error('Expected "lat,lon" format. Try {near: \'47.6689054,-122.3867575\'}');
}
lonLat = {
lat: parts[0],
lon: parts[1]
}
}
let nodeId = findIdNear(lonLat)
if (nodeId === undefined) {
throw new Error('Cannot find the node near ' + nearOption);
}
return nodeId;
}
function findIdNear(targetNode) {
if (!(Number.isFinite(targetNode.lon) && Number.isFinite(targetNode.lat))) {
return; // Something isn't right.
}
let minDistance = Infinity;
let minId = undefined;
nodes.forEach(node => {
let dist = euclid(node, targetNode);
if (dist < minDistance) {
minDistance = dist;
minId = node.id;
}
});
return minId;
}
}
function getDistanceFunction(optionsFunction) {
let distanceFunction = projected;
if (typeof optionsFunction === 'function') {
distanceFunction = optionsFunction;
} else if (distances[optionsFunction]) {
distanceFunction = distances[optionsFunction];
}
return distanceFunction;
}
function getGraphFromScene(scene) {
let mainLayer = scene.queryLayer();
let projector = mainLayer.grid.getProjector();
let nodes = mainLayer.grid.nodes;
let nodeIds = Array.from(nodes.keys());
let graph = toGraph(mainLayer)
return {projector, graph, nodes, nodeIds, mainLayer};
}
function canberra(node1, node2) {
return Math.abs(node1.lat - node2.lat)/(Math.abs(node1.lat) + Math.abs(node2.lat)) +
Math.abs(node1.lon - node2.lon)/(Math.abs(node1.lon) + Math.abs(node2.lon));
}
function manhattan(node1, node2) {
return Math.abs(node1.lat - node2.lat) + Math.abs(node1.lon - node2.lon);
}
function euclid(node1, node2) {
return Math.hypot(node1.lat - node2.lat, node1.lon - node2.lon);
}
function projected(node1, node2) {
let p = 0.017453292519943295; // Math.PI / 180
let c = Math.cos;
let a = 0.5 - c((node2.lat - node1.lat) * p)/2 +
c(node1.lat * p) * c(node2.lat * p) *
(1 - c((node2.lon - node1.lon) * p))/2;
return 12742 * Math.asin(Math.sqrt(a)); // 2 * R; R = 6371 km
}
module.exports.distances = distances;