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

302 lines (255 loc) 10.1 kB
'use strict'; var R = require('ramda'); var Zousan = require('zousan'); var buildStructureLookup = require('../../../src/utils/buildStructureLookup.js'); var configUtils = require('../../../src/utils/configUtils.js'); var i18n = require('../../../src/utils/i18n.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); async function create (app, config) { const log = app.log.sublog('poiDataManager'); const init = () => { app.bus.send('venueData/loadPoiData'); }; let poisLoaded = new Zousan(); const fixPositionInfo = (poi, structuresLookup) => { const { position } = poi; const structure = structuresLookup.floorIdToStructure(position.floorId); if (!structure) { log.error(`No structure found for floorId: ${position.floorId} for POI ${poi.poiId}`); return { ...poi } } const floor = structuresLookup.floorIdToFloor(position.floorId); const detailedPosition = { ...position, structureName: structure.name, buildingId: structure.id, floorName: floor.name, floorOrdinal: floor.ordinal }; return { ...poi, position: detailedPosition } }; // todo R.map may be enough to update dictionary values const formatPois = (pois, structuresLookup) => { return R__namespace.pipe( R__namespace.values, R__namespace.map(poi => { poi.distance = null; poi.isNavigable = poi.isNavigable === undefined || poi.isNavigable === true; // isNavigable is true as default, and is optional in the POI data // poi.isNavigable = /^[a-mA-M]+/.test(poi.name) // uncomment for easy testing of isNavigable if (poi.capacity) addToRoomInfo(poi, { name: `Seats ${poi.capacity.join('-')}`, svgId: 'number-of-seats' }); if (poi.category.startsWith('meeting')) addToRoomInfo(poi, { name: app.gt()('poiView:Conference Room'), svgId: 'conference-room' }); const roomId = getRoomId(poi); if (roomId) poi.roomId = roomId; return [poi.poiId, fixPositionInfo(poi, structuresLookup)] }), R__namespace.fromPairs )(pois) }; const addToRoomInfo = (poi, value) => { if (!poi.roomInfo) { poi.roomInfo = []; } poi.roomInfo.push(value); }; const getRoomId = R__namespace.pipe( R__namespace.propOr([], 'externalIds'), R__namespace.find(R__namespace.propEq('roomId', 'type')), R__namespace.prop('id'), R__namespace.unless(R__namespace.isNil, R__namespace.tail) ); async function checkNavgraph (pois) { const start = Date.now(); const badPois = []; const navGraph = await app.bus.get('wayfinder/_getNavGraph'); Object.values(pois).forEach(poi => { try { const pos = poi.position; const n = navGraph.findClosestNode(pos.floorId, pos.latitude, pos.longitude); if (!n) badPois.push({ id: poi.poiId, e: 'No closest Navgraph Node' }); } catch (e) { log.error(e); badPois.push({ id: poi.poiId, e: e.message }); } }); if (badPois.length) log.warn('badPois:', badPois); log(`Total time for navgraph POI check: ${Date.now() - start}ms`); } function filterBadPois (pois) { const badPois = []; Object.values(pois).forEach(poi => { try { const pos = poi.position; if (!pos) badPois.push({ poi, e: 'No position information' }); else { ;['buildingId', 'structureName', 'floorId', 'floorName', 'floorOrdinal', 'latitude', 'longitude'].forEach(k => { if (pos[k] === null || pos[k] === undefined) badPois.push({ id: poi.poiId, e: `invalid position property: ${k}: ${pos[k]}` }); }); } } catch (e) { log.error(e); badPois.push({ id: poi.poiId, e: e.message }); } }); if (badPois.length) { log.warn('badPois:', badPois); badPois.forEach(ob => { delete pois[ob.id]; }); } return pois } async function enhanceImages (pois) { for (const poi of Object.values(pois)) await addImages(poi); return pois } app.bus.on('venueData/poiDataLoaded', async ({ pois, structures }) => { const structuresLookup = buildStructureLookup.buildStructuresLookup(structures); pois = formatPois(pois, structuresLookup); if (configUtils.debugIsTrue(app, 'pseudoTransPois')) for (const id in pois) pois[id] = pseudoTransPoi(pois[id], app.i18n().language); pois = filterBadPois(pois); await enhanceImages(pois); poisLoaded.resolve(pois); if (app.config.debug && app.env.isBrowser) window._pois = pois; if (app.config.debug) checkNavgraph(pois); // this doesn't change the pois, but warns in the console... }); app.bus.on('poi/getById', async ({ id }) => { return poisLoaded.then(pois => pois[id]) }); app.bus.on('poi/getByFloorId', async ({ floorId }) => poisLoaded.then( R__namespace.pickBy(R__namespace.pathEq(floorId, ['position', 'floorId'])))); app.bus.on('poi/getByCategoryId', async ({ categoryId }) => { // returns true for exact category matches or parent category matches const categoryMatch = (poi) => poi.category === categoryId || poi.category.startsWith(categoryId + '.'); return poisLoaded.then(R__namespace.pickBy(categoryMatch)) }); app.bus.on('poi/getAll', async () => poisLoaded); const checkpointPath = ['queue', 'primaryQueueId']; const addOtherSecurityLanesIfNeeded = async poi => { const primaryCheckpointId = R__namespace.path(checkpointPath, poi); if (!primaryCheckpointId) return poi const queueTypes = await app.bus.get('venueData/getQueueTypes'); const securityPoisMap = await app.bus.get('poi/getByCategoryId', { categoryId: 'security' }); const securityPoisList = Object.values(securityPoisMap); poi.queue.otherQueues = prepareOtherSecurityLanes(queueTypes, poi, securityPoisList); return poi }; const prepareOtherSecurityLanes = (queueTypes, currentPoi, securityPois) => { const queueTypePath = ['queue', 'queueType']; const queueType = R__namespace.path(queueTypePath, currentPoi); if (!queueType) return null const queueSubtypes = queueTypes[queueType]; const primaryCheckpointId = R__namespace.path(checkpointPath, currentPoi); return securityPois .filter(R__namespace.pathEq(primaryCheckpointId, checkpointPath)) // filter only connected security checkpoints .filter(poi => poi.poiId !== currentPoi.poiId) // skip current poi .map(poi => { const laneId = R__namespace.path(['queue', 'queueSubtype'], poi); const lane = getLaneData(laneId)(queueSubtypes); return { poiId: poi.poiId, ...lane } }) }; // if the value passed is non-null, simply return it. Else throw the error with the message specified. // This is useful to insert into pipelines. const ensureDefined = errorMsg => value => { if (value != null) return value throw Error(errorMsg) }; const getLaneData = laneId => R__namespace.pipe( R__namespace.find(R__namespace.propEq(laneId, 'id')), ensureDefined(`No queue found with ID: ${laneId}`), R__namespace.pick(['displayText', 'imageId']) ); /** * Updates security checkpoint POI with a list of related security checkpoints. */ app.bus.on('poi/addOtherSecurityLanes', ({ poi }) => addOtherSecurityLanesIfNeeded(poi)); async function addImages (poi) { if (!poi) return const dpr = typeof window === 'undefined' ? 1 : (window.devicePixelRatio || 1); if (!R__namespace.length(poi.images)) { poi.images = []; } else if (!poi.images[0].startsWith('https:')) { // then images are not yet transformed poi.images = await Zousan.all( poi.images.map(imageName => app.bus.get('venueData/getPoiImageUrl', { imageName, size: `${Math.round(351 * dpr)}x${Math.round(197 * dpr)}` }))); } return poi } const getUniqueCategories = R__namespace.memoizeWith(R__namespace.identity, R__namespace.pipe(R__namespace.pluck('category'), R__namespace.values, R__namespace.uniq)); app.bus.on('poi/getAllCategories', async () => poisLoaded.then(getUniqueCategories)); app.bus.on('venueData/loadNewVenue', () => { poisLoaded = new Zousan(); init(); }); // See architectural document at https://docs.google.com/document/d/1NoBAboHR9BiX_vvLef-vp3ButrIQDWYofcTsdilEWvs/edit# app.bus.on('poi/setDynamicData', ({ plugin, idValuesMap }) => { poisLoaded .then(pois => { for (const poiId in idValuesMap) { // const dd = { [plugin]: idValuesMap[poiId] } const dynamicData = pois[poiId].dynamicData || {}; dynamicData[plugin] = { ...idValuesMap[poiId] }; const newPoi = R__namespace.mergeRight(pois[poiId], { dynamicData }); pois[poiId] = newPoi; } }); }); const runTest = async (testRoutine) => { await testRoutine(); return poisLoaded }; return { init, runTest, internal: { addImages, pseudoTransPoi } } } function pseudoTransPoi (poi, lang) { ['description', 'nearbyLandmark', 'name', 'phone', 'operationHours'] .forEach(p => { if (poi[p]) poi[p] = i18n.toLang(poi[p], lang); }); if (poi.keywords) poi.keywords = poi.keywords.map(keyword => { keyword.name = i18n.toLang(keyword.name, lang); return keyword }); if (poi.position.floorName) poi.position.floorName = i18n.toLang(poi.position.floorName, lang); if (poi.position.structureName) poi.position.structureName = i18n.toLang(poi.position.structureName, lang); return poi } exports.create = create;