UNPKG

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
'use strict'; 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;