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
593 lines (525 loc) • 21.2 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'));
const currentUnits = (await app.bus.getFirst('directions/getPreferredUnits')) || 'meters'; // can be 'meters' or 'yards'
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 }) => 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;