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

324 lines (274 loc) 10.7 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)); const isFullUrl = url => !!url?.startsWith('https:'); async function addImages(poi) { if (!poi) return; const dpr = typeof window === 'undefined' ? 1 : window.devicePixelRatio || 1; const size = `${Math.round(351 * dpr)}x${Math.round(197 * dpr)}`; if (!R__namespace.length(poi.images)) { poi.images = []; } else if (!isFullUrl(poi.images[0])) { // then images are not yet transformed poi.images = await Zousan.all( poi.images.map(imageName => app.bus.get('venueData/getPoiImageUrl', { imageName, size })), ); } if (!R__namespace.length(poi.fullImages)) { poi.fullImages = []; } else if (!isFullUrl(poi.fullImages[0]?.url)) { // then images are not yet transformed poi.fullImages = await Zousan.all( poi.fullImages.map(async ({ url, ...rest }) => { const fullUrl = await app.bus.get('venueData/getPoiImageUrl', { imageName: url, size, }); return { url: fullUrl, ...rest }; }), ); } 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;