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