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
296 lines (274 loc) • 12.1 kB
JavaScript
'use strict';
var R = require('ramda');
var bounds = require('../../../src/utils/bounds.js');
var geodesy = require('../../../src/utils/geodesy.js');
var segmentBadges = require('./segmentBadges.js');
var segmentCategories = require('./segmentCategories.js');
/**
* @typedef Step
* @property {string} primaryText
* @property {string} secondaryText
* @property {string} icon
* @property {Position} animationAnchor
* @property {number} eta
* @property {number} distance
* @property {boolean} isAccessible
* @property {Bounds} bounds
* @property {SecurityWaitTime} securityWaitTimes
*
* @typedef Position
* @property {number} lat
* @property {number} lng
* @property {string} floorId
* @property {number} ordinal
* @property {string} structureId
*
* @param {Segment[]} segments
* @param {string} startName
* @param {string} destinationName
* @param floorIdToNameMap
* @param {function} T - translate function
* @param {QueueTypes} queueTypes
* @param {boolean} requiresAccessibility
* @return {Step[]} steps - list of navigation steps
*/
function getSteps (segments, startName = '', destinationName = '', floorIdToNameMap, T, queueTypes = {}, requiresAccessibility, securityPois = []) {
return segments.map((segment, index) => {
const securityWaitTimes = findPropWaypoints('securityWaitTimes')(segment.waypoints);
const eta = (securityWaitTimes && !securityWaitTimes.isTemporarilyClosed) // if there is dynamic wait time and checkpoint is open, then use it
? securityWaitTimes.queueTime
: Math.round(calculateSegmentEta(segment.waypoints));
const distance = Math.round(calculateSegmentDistance(segment.waypoints));
const icon = getIcon(segment.segmentCategory);
const animationAnchor = getAnimationAnchor(segment.segmentCategory, segment.waypoints);
const primaryText = getPrimaryText(segments, index, startName, destinationName, floorIdToNameMap, T, securityPois, queueTypes);
const secondaryText = getSecondaryText(segment, eta, T);
const bounds$1 = bounds.findBoundsOfWaypoints(segment.waypoints);
const isAccessible = checkIfAccessible(segment);
const step = {
primaryText,
secondaryText,
icon,
animationAnchor,
eta,
distance,
bounds: bounds$1,
isAccessible,
securityWaitTimes
};
if (segment.poiId)
step.poiId = segment.poiId;
return step
})
}
function calculateSegmentEta (waypoints) {
if (waypoints.length === 1) return waypoints[0].eta // for stairs/elevator segments. should be removed once segment builder will be refactored to not append waypoint from previous semgnent
return waypoints
.map(R.prop('eta'))
.slice(1) // first waypoint is end of previous segment and is added only to connect segments for navline calculation
.reduce((segmentEta, eta) => segmentEta + eta, 0.0)
}
function calculateSegmentDistance (waypoints) {
if (waypoints.length === 1) return waypoints[0].distance // for stairs/elevator segments. should be removed once segment builder will be refactored to not append waypoint from previous semgnent
return waypoints
.map(waypoint => waypoint.distance)
.slice(1) // first waypoint is end of previous segment and is added only to connect segments for navline calculation
.reduce((segmentDistance, distance) => segmentDistance + distance, 0.0)
}
function getIcon (segmentCategory) {
switch (segmentCategory) {
case segmentCategories.START:
return segmentBadges.START
case segmentCategories.WALKING_TO_END:
return segmentBadges.END
case segmentCategories.ELEVATOR:
return segmentBadges.ELEVATOR
case segmentCategories.ELEVATOR_UP:
return segmentBadges.ELEVATOR_UP
case segmentCategories.ELEVATOR_DOWN:
return segmentBadges.ELEVATOR_DOWN
case segmentCategories.STAIRS:
return segmentBadges.STAIRS
case segmentCategories.STAIRS_UP:
return segmentBadges.STAIRS_UP
case segmentCategories.STAIRS_DOWN:
return segmentBadges.STAIRS_DOWN
case segmentCategories.ESCALATOR:
return segmentBadges.ESCALATOR
case segmentCategories.ESCALATOR_UP:
return segmentBadges.ESCALATOR_UP
case segmentCategories.ESCALATOR_DOWN:
return segmentBadges.ESCALATOR_DOWN
case segmentCategories.WALKING_TO_PORTAL:
case segmentCategories.WALK:
case segmentCategories.WALK_DOWN:
case segmentCategories.WALK_UP:
return segmentBadges.WALK
case segmentCategories.TRAIN:
return segmentBadges.TRAIN
case segmentCategories.TRAIN_UP:
return segmentBadges.TRAIN_UP
case segmentCategories.TRAIN_DOWN:
return segmentBadges.TRAIN_DOWN
case segmentCategories.BUS:
return segmentBadges.BUS
case segmentCategories.BUS_UP:
return segmentBadges.BUS_UP
case segmentCategories.BUS_DOWN:
return segmentBadges.BUS_DOWN
case segmentCategories.SECURITY_CHECKPOINT:
return segmentBadges.SECURITY_CHECKPOINT
case segmentCategories.RAMP:
return segmentBadges.RAMP
case segmentCategories.RAMP_UP:
return segmentBadges.RAMP_UP
case segmentCategories.RAMP_DOWN:
return segmentBadges.RAMP_DOWN
default:
return segmentBadges.WALK
}
}
function getAnimationAnchor (segmentCategory, waypoints) {
let index;
switch (segmentCategory) {
case segmentCategories.START:
index = 0;
break
case segmentCategories.WALKING_TO_END:
index = waypoints.length - 1;
break
case segmentCategories.WALKING_TO_PORTAL:
index = Math.min(waypoints.length - 1, Math.ceil(waypoints.length / 2));
break
default:
index = waypoints.length - 1;
}
return waypoints[index].position
}
function getPrimaryText (segments, index, startName, destinationName, floorIdToNameMap, T, securityPois, queueTypes) {
const segment = segments[index];
switch (segment.segmentCategory) {
case segmentCategories.START:
return getPositionName(segment.waypoints[0].position, startName)
case segmentCategories.WALKING_TO_END:
return getPositionName(segment.waypoints[segment.waypoints.length - 1].position, destinationName)
case segmentCategories.WALKING_TO_SECURITY_CHECKPOINT: {
const lookAheadWaypoints = segments[index + 1].waypoints;
const laneName = getSecurityLaneName(queueTypes, lookAheadWaypoints);
const translated = T(`wayfinder:Security Lane`);
const securityLane = laneName ? `${laneName} ${translated}` : null; // add translation
const poiTitle = getClosestSecurityPoiTitle(lookAheadWaypoints, securityPois);
const displayName = securityLane || poiTitle; // Use laneName if defined; otherwise, poiTitle
return displayName || T(`wayfinder:${segment.type}`)
}
case segmentCategories.WALKING_TO_PORTAL:
return T(`wayfinder:${segments[index + 1].type}`)
case segmentCategories.SECURITY_CHECKPOINT: {
const laneName = getSecurityLaneName(queueTypes, segment.waypoints);
const translated = T(`wayfinder:Security Lane`);
const securityLane = laneName ? `${laneName} ${translated}` : null;
const poiTitle = getClosestSecurityPoiTitle(segment.waypoints, securityPois);
const displayName = securityLane || poiTitle; // Use securityLane if defined; otherwise, poiTitle
return displayName || getLevelName(segment, floorIdToNameMap)
}
case segmentCategories.ELEVATOR:
case segmentCategories.ELEVATOR_DOWN:
case segmentCategories.ELEVATOR_UP:
case segmentCategories.ESCALATOR:
case segmentCategories.ESCALATOR_DOWN:
case segmentCategories.ESCALATOR_UP:
case segmentCategories.STAIRS:
case segmentCategories.STAIRS_DOWN:
case segmentCategories.STAIRS_UP:
return getLevelName(segment, floorIdToNameMap)
default: return T(`wayfinder:${segment.type}`)
}
}
function getPositionName (position, poiName) {
if (position.name) { return position.name }
return poiName
}
function getLevelName (segment, floorIdToNameMap) {
return floorIdToNameMap[R.last(segment.waypoints).position.floorId]
}
function getSecondaryText (segment, minutes, T) {
const zeroOrOtherKeys = translateZeroOrOther(minutes, T);
const travelVerb = 'Proceed';
switch (segment.segmentCategory) {
case segmentCategories.START: return T('wayfinder:Begin route at')
case segmentCategories.ELEVATOR: return T('wayfinder:Take elevator to')
case segmentCategories.ELEVATOR_UP: return T('wayfinder:Take elevator up to')
case segmentCategories.ELEVATOR_DOWN: return T('wayfinder:Take elevator down to')
case segmentCategories.STAIRS: return T('wayfinder:Take stairs to')
case segmentCategories.STAIRS_UP: return T('wayfinder:Take stairs up to')
case segmentCategories.STAIRS_DOWN: return T('wayfinder:Take stairs down to')
case segmentCategories.ESCALATOR: return T('wayfinder:Take escalator to')
case segmentCategories.ESCALATOR_UP: return T('wayfinder:Take escalator up to')
case segmentCategories.ESCALATOR_DOWN: return T('wayfinder:Take escalator down to')
case segmentCategories.WALK:
case segmentCategories.WALKING_TO_SECURITY_CHECKPOINT:
case segmentCategories.WALKING_TO_PORTAL:
case segmentCategories.WALKING_TO_END: return zeroOrOtherKeys(`${travelVerb} <1 minute to`, `${travelVerb} xx minute to`)
case segmentCategories.WALK_DOWN: return zeroOrOtherKeys(`${travelVerb} <1 minute down to`, `${travelVerb} xx minute down to`)
case segmentCategories.WALK_UP: return zeroOrOtherKeys(`${travelVerb} <1 minute up to`, `${travelVerb} xx minute up to`)
case segmentCategories.TRAIN: return zeroOrOtherKeys('Take train <1 minute', 'Take train xx minute')
case segmentCategories.BUS: return zeroOrOtherKeys('Take bus <1 minute', 'Take bus xx minute')
case segmentCategories.SECURITY_CHECKPOINT: {
return T('wayfinder:Through')
}
case segmentCategories.RAMP: return T('wayfinder:Take ramp to')
case segmentCategories.RAMP_UP: return T('wayfinder:Take ramp up to')
case segmentCategories.RAMP_DOWN: return T('wayfinder:Take ramp down to')
default: return ''
}
}
const translateZeroOrOther = (count, T) => (zeroKey, otherKey) => count === 0
? T('wayfinder:' + zeroKey)
: T('wayfinder:' + otherKey, { count });
const checkIfAccessible = R.compose(
R.not, R.includes(R.__, ['escalator', 'stairs']), R.toLower, R.prop('type'));
const getSecurityLaneName = (queueTypes, waypoints) => {
const lane = findPropWaypoints('securityLane')(waypoints);
if (!lane) return
const types = R.prop(lane.type, queueTypes);
const laneType = R.find(R.propEq(lane.id, 'id'), types);
return R.prop('displayText', laneType)
};
const findPropWaypoints = propName => R.compose(
R.prop(propName),
R.find(R.prop(propName)),
R.drop(1)); // because first waypoint is the end of previous segment and can be security checkpoint
/**
* Given an array of waypoints (with exactly one having portalType "Security Checkpoint")
* and an array of securityPois, this function uses the checkpoint's position
* to determine the closest security POI.
*
* @param {Array} waypoints - Array of waypoint objects.
* @param {Array} securityPois - Array of security POI objects.
* @returns {string|null} - The name of the closest security POI, or null if none found.
*/
const getClosestSecurityPoiTitle = (waypoints, securityPois) => {
if (!waypoints || waypoints.length === 0) return null
const checkpointWaypoint = waypoints[0];
const { lat, lng, floorId } = checkpointWaypoint.position;
// Use all POIs with a valid position on the same floor as the checkpoint.
const sameFloorCandidates = securityPois.filter(
poi => poi.position && poi.position.floorId === floorId
);
let closest = null;
let minDistance = Infinity;
sameFloorCandidates.forEach(poi => {
const d = geodesy.distance(
lat,
lng,
poi.position.latitude,
poi.position.longitude
);
if (d < minDistance) {
minDistance = d;
closest = poi;
}
});
return closest ? closest.name : null
};
module.exports = getSteps;