atriusmaps-node-sdk
Version:
This project provides an API to Atrius Personal Wayfinder maps within a Node environment. See the README.md for more information
296 lines (292 loc) • 10.6 kB
JavaScript
;
var R = require('ramda');
var geodesy = require('../../../src/utils/geodesy.js');
var geohasher = require('../../../src/extModules/geohasher.js');
var minPriorityQueue = require('./minPriorityQueue.js');
const DEFAULT_WALKING_SPEED_M_PER_MIN = 60;
const CLOSED_CHECKPOINT_EDGE_WEIGHT = 9999;
function createNavGraph(data, floorIdToOrdinal, floorIdToStructureId, securityLanesMap) {
const nodes = {};
const geoDb = {};
const nodesToAvoid = /* @__PURE__ */ new Set();
let securityWaitTimes = {};
data.nodes.forEach((nodeData) => {
const ordinal = floorIdToOrdinal(nodeData.floorId);
const structureId = floorIdToStructureId(nodeData.floorId);
const node = {
...R.pick(["id", "lat", "lng", "floorId"], nodeData),
edges: [],
ordinal,
structureId
};
addNode(node);
});
data.edges.forEach((ed) => nodes[ed.s].edges.push(createEdge(ed, nodes)));
function addNode(node) {
const largeGeo = node.floorId + ":" + geohasher.encode(node.lat, node.lng).substr(0, 7);
const mediumGeo = node.floorId + ":" + geohasher.encode(node.lat, node.lng).substr(0, 8);
if (!geoDb[largeGeo]) {
geoDb[largeGeo] = [];
}
geoDb[largeGeo].push(node);
if (!geoDb[mediumGeo]) {
geoDb[mediumGeo] = [];
}
geoDb[mediumGeo].push(node);
nodes[node.id] = node;
}
function createEdge(data2, nodes2) {
const type = getEdgeType(data2);
const typeLower = type.toLowerCase();
const isAccessible = typeLower !== "escalator" && typeLower !== "stairs";
const distance = distanceBetweenNodes(data2.s, data2.d, nodes2);
const transitTime = data2.l || distance / DEFAULT_WALKING_SPEED_M_PER_MIN;
const buildCurvedPath = (points) => points.map((point) => {
return {
start: { lat: point.s[0], lng: point.s[1] },
out: { lat: point.o[0], lng: point.o[1] },
in: { lat: point.i[0], lng: point.i[1] },
end: { lat: point.e[0], lng: point.e[1] }
};
});
const path = data2.p ? buildCurvedPath(data2.p) : null;
return {
distance,
dst: data2.d,
o: data2.o,
isAccessible,
isDriveway: !R.isNil(data2.h) && !data2.h,
src: data2.s,
transitTime,
type,
path,
weight: transitTime
};
}
function getEdgeType(data2) {
if (data2.x) {
return "Security Checkpoint";
}
if (data2.t === "") {
return "Ground";
}
return data2.t;
}
const findClosestNode = (endpoint) => {
if (endpoint.floorId === void 0 && endpoint.ordinal === void 0) {
throw Error("Endpoint specified in findRoute without floorId nor an ordinal");
}
const lat = endpoint.lat || endpoint.latitude;
const lng = endpoint.lng || endpoint.longitude;
return endpoint.floorId ? findClosestNodeByFloor(endpoint.floorId, lat, lng, geoDb, nodes) : findClosestNodeByOrdinal(endpoint.ordinal, lat, lng, nodes);
};
function findShortestPathEntry(start, end, nodes2, options = {}) {
const startNode = findClosestNode(start);
const endNode = findClosestNode(end);
return findShortestPath(startNode, endNode, nodes2, nodesToAvoid, securityWaitTimes, securityLanesMap, options);
}
function updateNodesToAvoid(nodes2) {
nodesToAvoid.clear();
nodes2.forEach((n) => nodesToAvoid.add(n));
}
function findAllShortestPaths(start, destArray, options) {
const startNode = findClosestNode(start);
const destNodeArray = destArray.map((dest) => findClosestNode(dest));
if (!startNode || !destNodeArray.length) {
return [];
}
return findAllShortestPathsImpl(
startNode,
destNodeArray,
nodes,
nodesToAvoid,
securityWaitTimes,
securityLanesMap,
options
);
}
function updateWithSecurityWaitTime(waitTimesData) {
securityWaitTimes = R.map(R.omit(["lastUpdated"]), waitTimesData);
clearCache();
}
return {
_nodes: nodes,
_geoDb: geoDb,
_nodesToAvoid: nodesToAvoid,
addNodesToAvoid: (nodes2) => updateNodesToAvoid(nodes2),
findClosestNode: (floorId, lat, lng) => findClosestNodeByFloor(floorId, lat, lng, geoDb, nodes),
findShortestPath: (start, end, options) => findShortestPathEntry(start, end, nodes, options),
findAllShortestPaths,
floorIdToOrdinal,
// todo lets get rid of this...
floorIdToStructureId,
// todo lets get rid of this...,
updateWithSecurityWaitTime,
clearCache
};
}
function distanceBetweenNodes(n1, n2, nodes) {
const node1 = nodes[n1];
const node2 = nodes[n2];
const distance = geodesy.distance(node1.lat, node1.lng, node2.lat, node2.lng);
return distance;
}
function findAllShortestPathsImpl(start, destinations, nodes, nodesToAvoid, securityWaitTimes = {}, securityLanesMap = {}, options = {}) {
return destinations.map((d) => {
try {
return findShortestPath(start, d, nodes, nodesToAvoid, securityWaitTimes, securityLanesMap, options);
} catch {
return null;
}
});
}
let cost, prev, visited, visitQueue, lastStartId, lastOptionsStr;
const clearCache = () => {
cost = {};
prev = {};
visited = {};
visitQueue = new minPriorityQueue();
lastStartId = null;
lastOptionsStr = {};
};
function findShortestPath(start, end, nodes, nodesToAvoid, securityWaitTimes = {}, securityLanesMap = {}, options = {}) {
if (start.id !== lastStartId || lastOptionsStr !== JSON.stringify(options)) {
clearCache();
visitQueue.offerWithPriority(start.id, 0);
cost[start.id] = 0;
visited[start.id] = true;
lastStartId = start.id;
lastOptionsStr = JSON.stringify(options);
}
while (!visitQueue.isEmpty() && !visited[end.id]) {
const node2 = nodes[visitQueue.poll()];
const ccost = cost[node2.id];
for (let ei = 0; ei < node2.edges.length; ei++) {
const e = node2.edges[ei];
if (nodesToAvoid.size > 0 && nodesToAvoid.has(e.dst)) {
continue;
}
if (visited[e.dst]) {
continue;
}
if (options.requiresAccessibility && !e.isAccessible) {
continue;
}
let weight = e.weight;
if (e.o && securityWaitTimes[e.o]) {
const dynamicData = securityWaitTimes[e.o];
if (dynamicData.queueTime) {
weight = dynamicData.queueTime;
}
if (dynamicData.isTemporarilyClosed) {
weight = CLOSED_CHECKPOINT_EDGE_WEIGHT;
}
e.securityWaitTimes = dynamicData;
}
if (e.o && securityLanesMap[e.o]) {
e.securityLane = securityLanesMap[e.o];
const { type, id } = securityLanesMap[e.o];
const securityLanesIds = R.path(["selectedSecurityLanes", type], options);
if (securityLanesIds && !securityLanesIds.includes(id)) {
continue;
}
}
if (cost[e.dst] === void 0) {
prev[e.dst] = node2;
cost[e.dst] = ccost + weight;
visitQueue.offerWithPriority(e.dst, ccost + weight);
} else if (cost[e.dst] > ccost + weight) {
cost[e.dst] = ccost + weight;
prev[e.dst] = node2;
visitQueue.raisePriority(e.dst, ccost + weight);
}
}
visited[node2.id] = true;
}
if (!visited[end.id]) {
return null;
}
const path = [];
let node = end;
while (node) {
path.push(node);
node = prev[node.id];
}
return path.reverse();
}
function geohashSearch(floorId, geohash, geoDb, size) {
const geohashPrefix = geohash.substr(0, size);
const searchGeos = [];
searchGeos.push(floorId + ":" + geohasher.calculateAdjacent(geohasher.calculateAdjacent(geohashPrefix, "top"), "left"));
searchGeos.push(floorId + ":" + geohasher.calculateAdjacent(geohashPrefix, "top"));
searchGeos.push(floorId + ":" + geohasher.calculateAdjacent(geohasher.calculateAdjacent(geohashPrefix, "top"), "right"));
searchGeos.push(floorId + ":" + geohasher.calculateAdjacent(geohashPrefix, "left"));
searchGeos.push(floorId + ":" + geohashPrefix);
searchGeos.push(floorId + ":" + geohasher.calculateAdjacent(geohashPrefix, "right"));
searchGeos.push(floorId + ":" + geohasher.calculateAdjacent(geohasher.calculateAdjacent(geohashPrefix, "bottom"), "left"));
searchGeos.push(floorId + ":" + geohasher.calculateAdjacent(geohashPrefix, "bottom"));
searchGeos.push(floorId + ":" + geohasher.calculateAdjacent(geohasher.calculateAdjacent(geohashPrefix, "bottom"), "right"));
const nodes = [];
for (let i = 0; i < searchGeos.length; i++) {
const nodesFound = geoDb[searchGeos[i]];
if (nodesFound) {
for (let j = 0; j < nodesFound.length; j++) {
nodes.push(nodesFound[j]);
}
}
}
return nodes;
}
function findNodesByGeohash(floorId, geohash, geoDb) {
let foundNodes = geohashSearch(floorId, geohash, geoDb, 8);
if (foundNodes.length > 0) {
return foundNodes;
}
foundNodes = geohashSearch(floorId, geohash, geoDb, 7);
if (foundNodes.length > 0) {
return foundNodes;
}
return null;
}
function findClosestNodeByFloor(floorId, lat, lng, geoDb, nodes) {
const cnodes = findNodesByGeohash(floorId, geohasher.encode(lat, lng), geoDb) || findClosestNodeByFloor2(floorId, lat, lng, nodes);
const nodeWithDistance = [];
for (let i = 0; i < cnodes.length; i++) {
const distance = geodesy.distance(lat, lng, cnodes[i].lat, cnodes[i].lng);
nodeWithDistance.push([cnodes[i], distance]);
}
nodeWithDistance.sort(function(a, b) {
return a[1] - b[1];
});
const nodesSortedByDistance = [];
for (let i = 0; i < nodeWithDistance.length; i++) {
nodesSortedByDistance.push(nodeWithDistance[i][0]);
}
return nodesSortedByDistance[0];
}
function findClosestNodeByFloor2(floorId, lat, lng, nodes) {
const floorNodes = Object.values(nodes).filter((n) => n.floorId === floorId).map((n) => [n, geodesy.distance(n.lat, n.lng, lat, lng)]);
if (!floorNodes.length) {
throw Error(`findClosestNodeByFloor2 found no nodes on floor ${floorId}`);
}
return selectShortest(floorNodes);
}
function findClosestNodeByOrdinal(ord, lat, lng, nodes) {
const ordNodes = Object.values(nodes).filter((n) => n.ordinal === ord).map((n) => [n, geodesy.distance(n.lat, n.lng, lat, lng)]);
if (!ordNodes.length) {
throw Error(`findClosestNodeByOrdinal found no nodes on ordinal ${ord}`);
}
return selectShortest(ordNodes);
}
function selectShortest(ar) {
let shortest = ar[0];
for (let i = 1; i < ar.length; i++) {
if (ar[i][1] < shortest[1]) {
shortest = ar[i];
}
}
return shortest[0];
}
exports.createNavGraph = createNavGraph;
exports.findClosestNodeByOrdinal = findClosestNodeByOrdinal;
exports.findShortestPath = findShortestPath;