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

557 lines (492 loc) 20.8 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); // todo may be not needed const SecurityLaneType = { SECURITY: 'SecurityLane', IMMIGRATION: 'ImmigrationLane' }; /** * @typedef {Object} Endpoint * @property {number} lat - latitude * @property {number} lng - longitude * @property {string} title * @property {string} [floorId] - usually present * @property {number} [ordinal] - optional * * @typedef SecurityLaneIdsMap * @property {string[]} SecurityLane - list of ids of security lanes * @property {string[]} ImmigrationLane - list of ids of immigration lanes * * @typedef Route * @property {Step[]} steps - list of navigation steps * @property {Segment[]} segments - list of navigation line segments * @property {number} time - total route time * @property {number} distance - total route distance * * @typedef RouteOptions * @property {SecurityLaneIdsMap} selectedSecurityLanes - map of selected lane ids by type * @property {boolean} requiresAccessibility - true if route should be accessible * @property {boolean} compareFindPaths - indicate whether to calculate path using 2 methods and then compare their performance * * @typedef SecurityWaitTime * @property {number} queueTime * @property {boolean} timeIsReal * @property {boolean} isTemporarilyClosed * * @typedef {Array<number>} Coordinate - pair of lng and lat * */ function create (app, config) { const log = app.log.sublog('wayfinder'); const init = async () => { app.bus.send('venueData/loadNavGraph'); }; let graphLoadedProm = new Zousan(); /** * Returns nav graph object for testing purposes. * Result includes nav nodes, edges, functions to update dynamic data and calculate shortest paths * * @returns {Object} */ app.bus.on('wayfinder/_getNavGraph', () => graphLoadedProm); /** * @typedef RawNavGraph * @property Array.<RawNavEdge> edges * @property Array.<RawNavNode> nodes * * @typedef RawNavEdge * @property {string} s - id of start node * @property {string} d - id of destination node * @property {number} l - custom transit time * @property {boolean} h - is edge a driveway * @property {string} t - edge type * @property {Array.<{ s, o, i, e }>|null} p - list of Bezier points * * @typedef RawNavNode * @property {string} id * @property {string} floorId * @property {number} lat * @property {number} lng * * Transforms raw nav graph data and list of structures * to nav graph object with functions to build shortest paths * * @param {RawNavGraph} navGraphData * @param {Array.<Structure>} structures */ 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 !== undefined && 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) }; /** * Returns a shortest path from user physical location to provided destination * and triggers rendering navigation line on the map * * @param {Endpoint} toEndpoint * @param {Boolean} requiresAccessibility * @param {SecurityLaneIdsMap} selectedSecurityLanes * @returns {Route} */ 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) }); /** * @busEvent wayfinder/getNavigationEndpoint * * Returns an object of the Endoint type. * wayfinding uses this structure to find the closest node * for shortestPath calculations, etc. * @param {Object} p - can be a POI (or similar) or a string with lat,lng[,floorId[,name]] or location defined as { latitutde, longitude [, floorId] [, title] } * @returns {Endpoint} navigational endpoint */ 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+$/)) // single integer - assume its poi id return poiIdToNavigationEndpoint(parseInt(p), graph.floorIdToOrdinal) if (p.indexOf(',') > 0) { // lat,lng,floorId,desc format let [lat, lng, floorId, title] = p.split(','); if (!graph.floorIdToStructureId(floorId)) throw Error('Unknown floorId in endpoint: ' + floorId) 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) // looks like a POI or some other 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 }); /** * Transforms provided data to endpoint type object * * @return {Endpoint} - navigation endpoint */ app.bus.on('wayfinder/getNavigationEndpoint', ({ ep }) => getNavigationEndpoint(ep)); /** * @typedef PathSecurityInfo * @property {boolean} routeExists * @property {boolean} [hasSecurity] * @property {boolean} [hasImmigration] * * Checks if there is a path between 2 endpoints which satisfies passed options * and if this path includes security and immigration lanes * * @param {Endpoint} fromEndpoint * @param {Endpoint} toEndpoint * @param {RouteOptions} options * @returns {PathSecurityInfo} */ 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); /** * @busEvent wayfinder/getRoute * * Builds the shortest path between 2 endpoints which satisfies passed options * * @param {RouteOptions} options * @param {Endpoint} fromEndpoint * @param {Endpoint} toEndpoint * * @return {(Route|null)} route - route or null if no route available */ async function getRoute ({ fromEndpoint, toEndpoint, options = {} }) { const rawPois = await app.bus.get('poi/getAll') || {}; // Extract the POI objects (assuming rawPois is an array with one object) const allPois = Array.isArray(rawPois) ? rawPois[0] : rawPois; // Convert the object values to an array and filter by category const securityPois = Object.values(allPois).filter( poi => poi.category && poi.category.startsWith('security') ); 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); 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 }) => total + distance, 0)); return { ...route, segments, steps, time, distance } }) } /** * Calculates transit time and distance of shortest path from each POI to start location which satisfies passed options * and returns list of copies of POI with these new properties * * @param {Endpoint} startLocation * @param {RouteOptions} options * @param pois: array of pois * @returns Array.<Object> - list of POIs */ 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 } /** * Calculates transit time and distance of shortest path from the start location to each POI * and calculates transit time and distance of shortest path from each POI to end location, * where both calculates must satisfy the passed options, * then adds the transit times and distances to create a total for each POI * and returns list of copies of POI with these new properties * * @param {Poi} startLocation * @param {Poi} endLocation * @param {Endpoint} currentLocation * @param {RouteOptions} options * @param pois: array of pois * @returns Array.<Object> - list of POIs */ 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 // ensure poi is reachable from both locations const timePrimary = resolvePathTime(pathPrimary, distancePrimary); const timeSecondary = resolvePathTime(pathSecondary, distanceSecondary); // create deep copy of poi and add information on the start and end locations 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) } /** * Resets plugin state */ app.bus.on('venueData/loadNewVenue', () => { graphLoadedProm = new Zousan(); init(); }); /** * Updates nav graph dynamic data if security data is passed * * @param {string} plugin - type of dynamic data * @param {Object<string, SecurityWaitTime|Object>} - dictionary of POI id to dynamic data object */ app.bus.on('poi/setDynamicData', ({ plugin, idValuesMap }) => { if (plugin !== 'security') return graphLoadedProm.then(graph => graph.updateWithSecurityWaitTime(idValuesMap)); }); /** * Returns a list of edges and nodes in a format convenient to display them on the map * * @typedef DebugNode * @property {string} floorId * @property {string} id * @property {boolean} isOrphaned * @property {number} lat * @property {number} lng * @property {number} ordinal * @property {string} structureId * * @typedef DebugEdge * @property {Coordinate} startCoordinates * @property {Coordinate} endCoordinates * @property {boolean} isDriveway * @property {number} ordinal * @property {string} category * @property {string} defaultStrokeColor * * @returns {{nodes: DebugNode[], edges: DebugEdge[]}} debug nav graph */ 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;