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

383 lines (357 loc) 14.9 kB
'use strict'; var bearing = require('@turf/bearing'); var helpers = require('@turf/helpers'); var R = require('ramda'); var bounds = require('../../../src/utils/bounds.js'); var distance = require('../../../src/utils/distance.js'); var geodesy = require('../../../src/utils/geodesy.js'); var segmentBadges = require('./segmentBadges.js'); var segmentCategories = require('./segmentCategories.js'); /** * Calculate the general direction of a segment based on its waypoints. * @param {Object} segment - The segment object. * @param {Array} segment.waypoints - An array of waypoints. * waypoint { * position: { lat, lng, floorId, ordinal, structureId }, * } * @returns {string} - The general direction of the segment. */ const calculateSegmentGeneralDirection = ({ waypoints }, T) => { const [firstWaypoint] = waypoints; const lastWaypoint = waypoints[waypoints.length - 1]; const from = helpers.point([firstWaypoint.position.lng, firstWaypoint.position.lat]); const to = helpers.point([lastWaypoint.position.lng, lastWaypoint.position.lat]); const bearingDegrees = bearing.bearing(from, to); // Convert bearing to general direction return geodesy.bearingToDirection(bearingDegrees, T); // returns cardinal direction 'north', 'northeast' etc as a string }; /** * @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 = [], currentUnits, ) { 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$1 = Math.round(calculateSegmentDistance(segment.waypoints)); // this is meters const currentUnitsDistance = currentUnits === 'yards' ? distance.metersToYards(distance$1) : distance$1; const icon = getIcon(segment.segmentCategory); const animationAnchor = getAnimationAnchor(segment.segmentCategory, segment.waypoints); const direction = calculateSegmentGeneralDirection(segment, T); // returns string like 'north', 'southwest', etc. const primaryText = getPrimaryText( segments, index, startName, destinationName, floorIdToNameMap, T, securityPois, queueTypes, ); const secondaryText = getSecondaryText(segment, eta, T, false); // this is old but apparently relied on by customers using JS SDK and Mobile SDK const secondaryTextUI = getSecondaryText(segment, eta, T, true, direction, currentUnits, currentUnitsDistance); // this is used in Webengine UI const bounds$1 = bounds.findBoundsOfWaypoints(segment.waypoints); const isAccessible = checkIfAccessible(segment); const step = { primaryText, secondaryText, secondaryTextUI, icon, animationAnchor, eta, distance: distance$1, 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, ui, direction, currentUnits, currentUnitsDistance) { 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: { if (!ui) { return zeroOrOtherKeys(`${travelVerb} <1 minute to`, `${travelVerb} xx minute to`); } else { return createSecondaryTextWithHeading(currentUnitsDistance, currentUnits, direction, T); } } 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 createSecondaryTextWithHeading = (currentUnitsDistance, currentUnits, direction, T) => { if (currentUnitsDistance > 1) { return currentUnits === 'meters' ? T(`wayfinder:Proceed __direction__ __count__ meter to_plural`, { direction, count: currentUnitsDistance, }) : T(`wayfinder:Proceed __direction__ __count__ yard to_plural`, { direction, count: currentUnitsDistance, }); } if (currentUnitsDistance <= 1) { return currentUnits === 'meters' ? T(`wayfinder:Proceed __direction__ __count__ meter to`, { direction, count: currentUnitsDistance, }) : T(`wayfinder:Proceed __direction__ __count__ yard to`, { direction, count: currentUnitsDistance, }); } }; 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;