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

296 lines (274 loc) 12.1 kB
'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;