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

379 lines (373 loc) 15.1 kB
'use strict'; var R = require('ramda'); var Zousan = require('zousan'); var buildStructureLookup = require('../../../src/utils/buildStructureLookup.js'); var geodesy = require('../../../src/utils/geodesy.js'); var findRoute = require('./findRoute.js'); var navGraph = require('./navGraph.js'); var navGraphDebug = require('./navGraphDebug.js'); var segmentBuilder = require('./segmentBuilder.js'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var R__namespace = /*#__PURE__*/_interopNamespaceDefault(R); const DEFAULT_WALKING_SPEED_M_PER_MIN = 60; const getEdgeTo = (dst) => (node) => R__namespace.find((e) => e.dst === dst, node.edges); const SecurityLaneType = { SECURITY: "SecurityLane", IMMIGRATION: "ImmigrationLane" }; function create(app, config) { const log = app.log.sublog("wayfinder"); const init = async () => { app.bus.send("venueData/loadNavGraph"); }; let graphLoadedProm = new Zousan(); app.bus.on("wayfinder/_getNavGraph", () => graphLoadedProm); app.bus.on("venueData/navGraphLoaded", async ({ navGraphData, structures }) => { const structureLookup = buildStructureLookup.buildStructuresLookup(structures); const securityLanesMap = await prepareSecurityLanes(); const graph = navGraph.createNavGraph( navGraphData, structureLookup.floorIdToOrdinal, structureLookup.floorIdToStructureId, securityLanesMap ); graphLoadedProm.resolve(graph); }); app.bus.on("poi/setDynamicRouting", async ({ idValuesMap }) => { const graph = await graphLoadedProm; const poisToAvoid = Object.values(idValuesMap); const nodesToAvoid = poisToAvoid.filter( (poi) => poi.position && poi.position.floorId !== void 0 && poi.position.latitude && poi.position.longitude ).map((poi) => graph.findClosestNode(poi.position.floorId, poi.position.latitude, poi.position.longitude).id); graph.addNodesToAvoid(nodesToAvoid); }); const prepareSecurityLanes = async () => { const securityPois = await app.bus.get("poi/getByCategoryId", { categoryId: "security" }); return R__namespace.pipe(R__namespace.map(getSecurityLane), R__namespace.filter(R__namespace.identity))(securityPois); }; const getSecurityLane = (poi) => poi.queue && { type: R__namespace.path(["queue", "queueType"], poi), id: R__namespace.path(["queue", "queueSubtype"], poi) }; app.bus.on( "wayfinder/showNavLineFromPhysicalLocation", async ({ toEndpoint, selectedSecurityLanes = null, requiresAccessibility }) => { const physicalLocation = await app.bus.getFirst("user/getPhysicalLocation"); return navigateFromTo(physicalLocation, toEndpoint, { selectedSecurityLanes, requiresAccessibility, primary: true }); } ); async function navigateFromTo(fromEndpoint, toEndpoint, options) { const route = await getRoute({ fromEndpoint, toEndpoint, options }); if (route) { const { segments } = route; if (options.primary) { app.bus.send("map/resetNavlineFeatures"); } app.bus.send("map/showNavlineFeatures", { segments, category: options.primary ? "primary" : "alternative" }); } return route; } const poiIdToNavigationEndpoint = (id, floorIdToOrdinal) => app.bus.get("poi/getById", { id }).then((poi) => { if (poi && poi.position) { return poiToNavigationEndpoint(poi, floorIdToOrdinal); } else { throw Error("Unknown POI ID " + id); } }); async function getNavigationEndpoint(p) { return graphLoadedProm.then((graph) => { if (!p) { throw Error("wayfinder: Invalid endpoint definition", p); } if (typeof p === "number") { return poiIdToNavigationEndpoint(p, graph.floorIdToOrdinal); } if (typeof p === "string") { if (p.match(/^\d+$/)) { return poiIdToNavigationEndpoint(parseInt(p), graph.floorIdToOrdinal); } if (p.indexOf(",") > 0) { const [lat, lng, floorId, initialTitle] = p.split(","); if (!graph.floorIdToStructureId(floorId)) { throw Error("Unknown floorId in endpoint: " + floorId); } let title = initialTitle; if (!title) { title = "Starting Point"; } return { lat: parseFloat(lat), lng: parseFloat(lng), ordinal: graph.floorIdToOrdinal(floorId), floorId, title }; } } if (isEndpoint(p)) { return p; } if (p.latitude) { return { lat: p.latitude, lng: p.longitude, floorId: p.floorId, ordinal: graph.floorIdToOrdinal(p.floorId), title: p.title }; } if (p.position && p.name) { return poiToNavigationEndpoint(p, graph.floorIdToOrdinal); } throw Error("Invalid start or end point: " + p); }); } const endpointProps = ["lat", "lng", "floorId", "ordinal"]; const isEndpoint = R__namespace.pipe(R__namespace.pick(endpointProps), R__namespace.keys, R__namespace.propEq(endpointProps.length, "length"), Boolean); const poiToNavigationEndpoint = (poi, floorIdToOrdinal) => ({ lat: poi.position.latitude, lng: poi.position.longitude, floorId: poi.position.floorId, ordinal: floorIdToOrdinal(poi.position.floorId), title: poi.name }); app.bus.on("wayfinder/getNavigationEndpoint", ({ ep }) => getNavigationEndpoint(ep)); app.bus.on( "wayfinder/checkIfPathHasSecurity", ({ fromEndpoint, toEndpoint, options = {} }) => graphLoadedProm.then((graph) => { options.compareFindPaths = config.compareFindPaths; const route = findRoute.findRoute(graph, fromEndpoint, toEndpoint, options); if (!route) { return { routeExists: false }; } const queues = route.waypoints.filter( (node) => R__namespace.pathEq(SecurityLaneType.SECURITY, ["securityLane", "type"], node) || R__namespace.pathEq(SecurityLaneType.IMMIGRATION, ["securityLane", "type"], node) ); const containsSecurityLaneType = (type) => Boolean(route.waypoints.find(R__namespace.pathEq(type, ["securityLane", "type"]))); return { routeExists: true, queues, hasSecurity: containsSecurityLaneType(SecurityLaneType.SECURITY), hasImmigration: containsSecurityLaneType(SecurityLaneType.IMMIGRATION) }; }) ); app.bus.on("wayfinder/getRoute", getRoute); async function getRoute({ fromEndpoint, toEndpoint, options = {} }) { const rawPois = await app.bus.get("poi/getAll") || {}; const allPois = Array.isArray(rawPois) ? rawPois[0] : rawPois; const securityPois = Object.values(allPois).filter((poi) => poi.category && poi.category.startsWith("security")); const currentUnits = await app.bus.getFirst("directions/getPreferredUnits") || "meters"; return graphLoadedProm.then(async (graph) => { options.compareFindPaths = config.compareFindPaths; const route = findRoute.findRoute(graph, fromEndpoint, toEndpoint, options); if (!route) { return null; } const floorIdToNameMap = await app.bus.get("venueData/getFloorIdToNameMap"); const queueTypes = await app.bus.get("venueData/getQueueTypes"); const translate = app.gt(); const isAccessible = options.requiresAccessibility; const { steps, segments } = segmentBuilder.buildSegments( route.waypoints, fromEndpoint, toEndpoint, floorIdToNameMap, translate, queueTypes, isAccessible, securityPois, currentUnits ); log.info("route", route); const time = Math.round(route.waypoints.reduce((total, { eta }) => total + eta, 0)); const distance = Math.round(route.waypoints.reduce((total, { distance: distance2 }) => total + distance2, 0)); return { ...route, segments, steps, time, distance }; }); } app.bus.on("wayfinder/addPathTimeMultiple", async ({ pois, startLocation, options = {} }) => { if (!startLocation) { return pois; } return graphLoadedProm.then((graph) => addPathTimeMultiple(graph, options, pois, startLocation)); }); function addPathTimeMultiple(graph, options, pois, start) { try { const poisList = R__namespace.clone(pois); const poiLocations = poisList.map((poi) => poiToNavigationEndpoint(poi, graph.floorIdToOrdinal)); const paths = graph.findAllShortestPaths(start, poiLocations, options); const poisWithPathProps = poisList.map((poi, i) => resolveAndAddPathProps(poi, paths[i], start, "start")); return filterAndSort(poisWithPathProps); } catch (e) { log.error(e); return pois; } } function resolveAndAddPathProps(poi, path, endpoint, endpointType) { let updatedPoi = R__namespace.clone(poi); updatedPoi = addEndpointInformation(updatedPoi, endpoint, endpointType); if (path && path.length) { return { ...updatedPoi, transitTime: calculateTotalPathProperty(path, "transitTime"), distance: calculateTotalPathProperty(path, "distance") }; } else { updatedPoi.distance = endpointType === "start" ? getGeoDistance(updatedPoi, endpoint) : getGeoDistance(endpoint, updatedPoi); updatedPoi.transitTime = getTransitTime(updatedPoi.distance); return updatedPoi; } } function calculateTotalPathProperty(path, propertyName) { return R__namespace.aperture(2, path).map(([from, to]) => getEdgeTo(to.id)(from)).map(R__namespace.prop(propertyName)).reduce((totalTime, edgeTime) => totalTime + edgeTime, 0); } function addEndpointInformation(poi, endpoint, endpointType) { return { ...poi, [endpointType + "Information"]: { lat: endpoint?.lat || endpoint?.position?.latitude, lng: endpoint?.lng || endpoint?.position?.longitude, floorId: endpoint?.floorId || endpoint?.position?.floorId } }; } function getGeoDistance(endLocation, startLocation) { return geodesy.distance( startLocation?.lat || startLocation?.position?.latitude, startLocation?.lng || startLocation?.position?.longitude, endLocation?.lat || endLocation?.position?.latitude, endLocation?.lng || endLocation?.position?.longitude ); } function getTransitTime(distance) { return distance / DEFAULT_WALKING_SPEED_M_PER_MIN; } app.bus.on( "wayfinder/multipointAddPathTimeMultiple", async ({ pois, startLocation, endLocation, currentLocation, options = {} }) => { if (!startLocation && !endLocation && !currentLocation) { return pois; } return graphLoadedProm.then( (graph) => multipointAddPathTimeMultiple(graph, options, pois, startLocation, endLocation, currentLocation) ); } ); function multipointAddPathTimeMultiple(graph, options, pois, startPoi, endPoi, currentLocation) { try { const start = startPoi ? poiToNavigationEndpoint(startPoi, graph.floorIdToOrdinal) : currentLocation; const end = endPoi ? poiToNavigationEndpoint(endPoi, graph.floorIdToOrdinal) : null; const poisList = R__namespace.clone(pois); const poiLocations = poisList.map((poi) => poiToNavigationEndpoint(poi, graph.floorIdToOrdinal)); let pathsPrimary, pathsSecondary; if (start) { pathsPrimary = graph.findAllShortestPaths(start, poiLocations, options); } if (end) { pathsSecondary = getAllSecondaryPaths(graph, poiLocations, end, options); } let poisWithPathProps; if (start && end) { poisWithPathProps = poisList.map( (poi, i) => resolveAndAddMultipointPathProps(poi, pathsPrimary[i], pathsSecondary[i], start, end) ); } else if (start) { poisWithPathProps = poisList.map((poi, i) => resolveAndAddPathProps(poi, pathsPrimary[i], start, "start")); } else { poisWithPathProps = poisList.map((poi, i) => resolveAndAddPathProps(poi, pathsSecondary[i], end, "end")); } return filterAndSort(poisWithPathProps); } catch (e) { log.error(e); return pois; } } function filterAndSort(pois) { const poisWithPathProps = pois.filter((poi) => poi !== null); return R__namespace.sortBy(R__namespace.propOr(Infinity, "transitTime"), poisWithPathProps); } function resolveAndAddMultipointPathProps(poi, pathPrimary, pathSecondary, startLocation, endLocation) { const distancePrimary = resolvePathDistance(pathPrimary, startLocation, poi); const distanceSecondary = resolvePathDistance(pathSecondary, poi, endLocation); if (!distancePrimary || !distanceSecondary) { return null; } const timePrimary = resolvePathTime(pathPrimary, distancePrimary); const timeSecondary = resolvePathTime(pathSecondary, distanceSecondary); let updatedPoi = R__namespace.clone(poi); updatedPoi = addEndpointInformation(updatedPoi, startLocation, "start"); updatedPoi = addEndpointInformation(updatedPoi, endLocation, "end"); return { ...updatedPoi, transitTime: timePrimary + timeSecondary, distance: distancePrimary + distanceSecondary, startInformation: { ...updatedPoi.startInformation, transitTime: timePrimary, distance: distancePrimary }, endInformation: { ...updatedPoi.endInformation, transitTime: timeSecondary, distance: distanceSecondary } }; } function getAllSecondaryPaths(graph, poiLocations, endLocation, options) { const pathsSecondary = []; for (const pointAsStart of poiLocations) { pathsSecondary.push(graph.findShortestPath(pointAsStart, endLocation, options)); } return pathsSecondary; } function resolvePathTime(path, distance) { return path && path.length ? calculateTotalPathProperty(path, "transitTime") : getTransitTime(distance); } function resolvePathDistance(path, startLocation, endLocation) { return path && path.length ? calculateTotalPathProperty(path, "distance") : getGeoDistance(endLocation, startLocation); } app.bus.on("venueData/loadNewVenue", () => { graphLoadedProm = new Zousan(); init(); }); app.bus.on("poi/setDynamicData", ({ plugin, idValuesMap }) => { if (plugin !== "security") { return; } graphLoadedProm.then((graph) => graph.updateWithSecurityWaitTime(idValuesMap)); }); app.bus.on("wayfinder/getNavGraphFeatures", () => graphLoadedProm.then(({ _nodes }) => navGraphDebug.enrichDebugNavGraph(_nodes))); return { init, internal: { resolveNavGraph: (graph) => graphLoadedProm.resolve(graph), prepareSecurityLanes } }; } exports.SecurityLaneType = SecurityLaneType; exports.create = create;