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
JavaScript
;
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;