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

393 lines (387 loc) 16.6 kB
'use strict'; var R = require('ramda'); var Zousan = require('zousan'); var venueLoadingUtils = require('./venueLoadingUtils.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); const USE_AUTH_WHEN_NOT_PROD_STAGE = false; async function create(app, config) { const log = app.log.sublog("venueDataLoader"); let venueDataLoaded = new Zousan(); let mapDataLoaded = new Zousan(); const getDefaultStructureId = (venueData) => venueData.defaultStructureId || R__namespace.path(["structureOrder", 0], venueData) || R__namespace.path(["selectorOrder", 0], venueData) || R__namespace.pipe(R__namespace.prop("structures"), Object.values, R__namespace.path([0, "id"]))(venueData); const mergeWithProp = (prop, toAdd, o) => R__namespace.over(R__namespace.lensProp(prop), (old) => R__namespace.mergeRight(old || {}, toAdd), o); const countMatchedPrefix = (a, b) => a.split("").reduce((max, _, i) => a.substring(0, i + 1) === b.substring(0, i + 1) ? i : max, -1) + 1; const getVenueDefForLang = (venueData, lang) => Object.values(venueData.venueList).filter((venueDef) => venueDef.locale === lang).sort((a, b) => countMatchedPrefix(b.id, venueData.id) - countMatchedPrefix(a.id, venueData.id)).at(0); const calculateBaseVenueId = (venueData) => getVenueDefForLang(venueData, "en")?.id; async function loadVenueData(vConfig, languagesToTry) { if (vConfig.assetStage && vConfig.assetStage !== "prod" && location.hostname !== "localhost" && USE_AUTH_WHEN_NOT_PROD_STAGE) ; const fetchJson = venueLoadingUtils.createFetchJson(); const fetchText = venueLoadingUtils.createFetchText(); const venueData = await venueLoadingUtils.getVenueDataFromUrls(vConfig, fetchJson, languagesToTry); const { accountId, venueId } = vConfig; venueData.assetStage = vConfig.assetStage; venueData.defaultOrdinal = venueData.defaultOrdinal || getDefaultOrdinal(venueData); venueData.structures = venueLoadingUtils.buildStructures(venueData); venueData.baseVenueId = calculateBaseVenueId(venueData); venueData.getTranslatedContentPath = (contentType) => `https://content.locuslabs.com/${venueData.category}/${contentType}/${venueId}/${accountId}`; venueData.fetchJson = fetchJson; venueData.fetchText = fetchText; if (app.config.debug && app.env.isBrowser) { window._venueData = venueData; } if (venueData.queueTypes) { venueData.securityQueueTypes = (() => { const secLane = venueData.queueTypes.find((qt) => qt.id === "SecurityLane"); if (secLane) { return secLane.subtypes.map((t) => t.id); } return []; })(); } app.bus.send("venueData/venueDataLoaded", { venueData }); venueDataLoaded.resolve(venueData); return venueDataLoaded; } function getDefaultOrdinal(venueData) { const defaultStructureId = getDefaultStructureId(venueData); const defaultStructure = Object.values(venueData.structures).find(R__namespace.propEq(defaultStructureId, "id")); return defaultStructure.levels[defaultStructure.defaultLevelId].ordinal; } function notifyState(venueData) { const state = { id: "venueDataLoader" }; if (venueData.id !== config.venueId) { state.vid = venueData.id; } state.lang = app.i18n().language; if (venueData.assetStage !== "prod") { state.stage = venueData.assetStage; } app.bus.send("deepLinking/notifyState", state); return venueData; } app.bus.on("debugTools/fileDrop", async ({ file, content }) => { if (file.type === "application/json") { const jsonOb = JSON.parse(content); if (jsonOb.basemap && jsonOb["basemap.venue"]) { return replaceTheme(JSON.parse(content)); } if (jsonOb.metadata && jsonOb.metadata["mapbox:type"]) { return replaceStyle(content); } } }); const withinCategory = (c1, c2) => c1 === c2 || c1.indexOf(c2 + ".") === 0; function poiMapNameXForm(poi) { let name = poi.name; if (!config.poiMapNameXForm) { return name; } Object.keys(config.poiMapNameXForm).filter((c2) => withinCategory(poi.category, c2)).forEach((c2) => { const xforms = config.poiMapNameXForm[c2]; xforms.forEach((xform) => name = name.replace(new RegExp(xform.replace), xform.with)); }); return name; } async function copyPOINames(mapFeatures) { const newMapFeatures = { ...mapFeatures }; const pois = await app.bus.get("poi/getAll"); Object.values(newMapFeatures).forEach( (layerArray) => layerArray.filter((f) => f.properties.aiLayer === "poi" && f.geometry.type === "Point").forEach((f) => { const poi = pois[f.properties.id]; if (!poi) { log.warn(`Unknown poi in style: ${f.properties.id}`); } else { if (poi.mapLabel) { f.properties.text = poi.mapLabel; } else if (config.copyPOINamesToMap !== false) { f.properties.text = poiMapNameXForm(poi); } } }) ); return newMapFeatures; } function replaceTheme(mapThemeSource) { app.bus.send("map/replaceTheme", { theme: mapThemeSource }); } function replaceStyle(mapStyleSource) { app.bus.send("map/replaceStyle", { styleSrc: mapStyleSource }); } async function getMapFeatures(venueData) { return R__namespace.pipe( R__namespace.prop("structures"), R__namespace.map(R__namespace.prop("levels")), R__namespace.chain(R__namespace.keys), // Generate list of level IDs plus the venue ID to fetch feature JSON for each R__namespace.prepend(venueData.id), // eslint-disable-next-line no-template-curly-in-string R__namespace.map((geoJsonId) => venueData.files.geoJson.replace("${geoJsonId}", geoJsonId)), R__namespace.map(venueData.fetchJson), R__namespace.map( R__namespace.andThen((featureJSON) => [ featureJSON.id, enrichFeaturesForLevel(featureJSON.id, featureJSON.features, venueData.id, venueData.structures) ]) ), (promises) => Zousan.all(promises), R__namespace.andThen(R__namespace.fromPairs) )(venueData); } const enrichFeaturesForLevel = (levelId, features, venueId, structures) => { const structureId = levelId.replace(/-[^-]*$/, ""); const ordinalId = structureId === venueId ? "landscape-background" : `ordinal: ${structures.find(R__namespace.hasPath(["levels", levelId])).levels[levelId].ordinal}`; const enrichFeature = (feature) => { feature = mergeWithProp("properties", { venueId, structureId, ordinalId, levelId }, feature); feature = R__namespace.assoc("id", feature.properties.subid, feature); return feature; }; return features.map(enrichFeature); }; app.bus.on("venueData/loadMap", async () => { venueDataLoaded.then(async (venueData) => { const mapStyleSource = await venueData.fetchText(venueData.files.style); const mapTheme = await venueData.fetchJson(venueData.files.theme); const badgesSpriteUrl = venueData.files.spritesheet; const mapGlyphsUrl = venueData.files.glyphs; const { id, bounds, structures, venueCenter, venueRadius, defaultOrdinal } = venueData; const mapFeatures = venueData.venueList[id].mapTokens ? { [id]: [] } : await getMapFeatures(venueData).then(copyPOINames); const venueBounds = { n: bounds.ne.lat, s: bounds.sw.lat, e: bounds.ne.lng, w: bounds.sw.lng }; const mapData = { mapFeatures, mapStyleSource, mapTheme, badgesSpriteUrl, mapGlyphsUrl, structures, defaultOrdinal, venueBounds, venueId: id, venueCenter, venueRadius, accountId: config.accountId, secure: config.auth !== void 0, tileServerAuthInfo: venueData.tileServerAuthInfo // only defined for tile maps }; mapDataLoaded.resolve(mapData); app.bus.send("venueData/mapDataLoaded", mapData); }); }); const shouldDisplayPredicate = (building) => building.shouldDisplay == null || building.shouldDisplay; app.bus.on("venueData/loadBuildingSelectorData", () => { return venueDataLoaded.then(async (venueData) => { const buildings = venueData.structures.filter(shouldDisplayPredicate).map( R__namespace.evolve({ // creates copy of structure with modified levels levels: R__namespace.pipe(R__namespace.values, R__namespace.sortWith([R__namespace.descend(R__namespace.prop("ordinal"))])) }) ); const result = { buildings, structureOrder: venueData.structureOrder, selectorOrder: venueData.selectorOrder }; app.bus.send("venueData/buildingSelectorDataLoaded", result); return result; }); }); app.bus.on( "venueData/normalizeCoords", ({ coords }) => venueDataLoaded.then((venueData) => venueLoadingUtils.normalizeCoords(coords, venueData.bounds)) ); const noNavInfo = { edges: [], nodes: [] }; app.bus.on("venueData/loadNavGraph", async () => { return venueDataLoaded.then(async (venueData) => { const navGraphData = venueData.files.nav ? await venueData.fetchJson(venueData.files.nav) : noNavInfo; const navGraphOb = { navGraphData, structures: venueData.structures }; app.bus.send("venueData/navGraphLoaded", navGraphOb); return navGraphOb; }); }); app.bus.on("venueData/loadPoiData", async () => { return venueDataLoaded.then(async (venueData) => { const poisUrl = config.useOldDataModel ? venueData.files.poisOld || venueData.files.pois : venueData.files.pois || venueData.files.poisOld; if (poisUrl) { const pois = await venueData.fetchJson(poisUrl); app.bus.send("venueData/poiDataLoaded", { pois, structures: venueData.structures }); } }); }); app.bus.on( "venueData/getVenueCenter", async () => venueDataLoaded.then(async (venueData) => ({ lat: venueData.venueCenter[0], lng: venueData.venueCenter[1], ordinal: 0 })) ); app.bus.on( "venueData/getContentUrl", ({ type, name = "" }) => venueDataLoaded.then((venueData) => venueData.files[type] + name) ); app.bus.on( "venueData/getFloorIdToNameMap", () => venueDataLoaded.then( R__namespace.pipe( R__namespace.prop("structures"), R__namespace.map(R__namespace.prop("levels")), // get levels for each structure R__namespace.chain(R__namespace.values), // flatten structures levels into single array R__namespace.map(R__namespace.props(["id", "name"])), // create pairs [id, name] R__namespace.fromPairs // create map of 'id' to 'name' ) ) ); app.bus.on("venueData/getFloorIdName", ({ floorId }) => { return venueDataLoaded.then(async (venueData) => { const structure = R__namespace.pipe(R__namespace.values, R__namespace.find(R__namespace.hasPath(["levels", floorId])))(venueData.structures); if (!structure) { return null; } return { structureId: structure.id, structureName: structure.name, floorName: structure.levels[floorId].name }; }); }); const getVenueDataProp = (propName, defaultValue) => () => venueDataLoaded.then(R__namespace.pipe(R__namespace.prop(propName), R__namespace.defaultTo(defaultValue))); app.bus.on("venueData/getVenueData", () => venueDataLoaded); app.bus.on("venueData/getVenueName", getVenueDataProp("name")); app.bus.on("venueData/getVenueCategory", getVenueDataProp("category")); app.bus.on("venueData/getVenueTimezone", getVenueDataProp("tz")); app.bus.on("venueData/getAccountId", () => config.accountId); app.bus.on("venueData/getVenueId", getVenueDataProp("id")); app.bus.on("venueData/getPositioningSupported", getVenueDataProp("positioningSupported")); app.bus.on("venueData/getStructures", getVenueDataProp("structures")); app.bus.on("venueData/loadNewVenue", async ({ venueId, accountId, assetStage = config.assetStage }) => { venueDataLoaded.reject(new Error("loadNewVenue called - previous loading ignored")); mapDataLoaded.reject(new Error("loadNewVenue called - previous loading ignored")); venueDataLoaded = new Zousan(); mapDataLoaded = new Zousan(); loadVenueData({ ...config, venueId, accountId, assetStage }, []).then(notifyState); }); app.bus.on("venueData/getPoiImageUrl", ({ imageName, size }) => { return `https://img.locuslabs.com/resize/${config.accountId}/png/transparent/${size}contain/poi/${imageName}`; }); app.bus.on("venueData/getDistributionId", () => { return venueDataLoaded.then((venueData) => { return `${venueData.baseVenueId}-${config.accountId}`; }); }); app.bus.on( "venueData/getCustomKeywords", () => venueDataLoaded.then((venueData) => { const searchUrl = config.useOldDataModel ? venueData.files.searchOld || venueData.files.search : venueData.files.search; return venueData.fetchJson(searchUrl); }) ); app.bus.on("venueData/isGrabEnabled", getVenueDataProp("enableGrab")); app.bus.on("venueData/getGrabPoiIds", getVenueDataProp("grabPoiIds", [])); app.bus.on("venueData/getAssetsTimestamp", getVenueDataProp("version")); app.bus.on( "venueData/getTranslatedFloorId", async ({ floorId }) => venueDataLoaded.then((venueData) => getTranslatedFloorId(floorId, venueData)) ); function getTranslatedFloorId(floorId, venueData) { const allFloorIds = getAllFloorIds(venueData); if (allFloorIds.includes(floorId)) { return floorId; } const buildingLevel = floorId.split("-").slice(1).join("-"); return allFloorIds.find((fid) => new RegExp(`${venueData.baseVenueId}.?.?-${buildingLevel}`).test(fid)); } const getAllFloorIds = (venueData) => venueData.structures.reduce((floors, s) => floors.concat(Object.keys(s.levels)), []); app.bus.on("venueData/getQueueTypes", () => { return venueDataLoaded.then((venueData) => { const lanesWithImages = ["tsapre", "clear", "globalEntry"]; if (venueData.queueTypes) { return venueData.queueTypes.reduce((obj, category) => { const { id, subtypes } = category; const typesWithImages = subtypes.map((type) => { const imageId = lanesWithImages.includes(type.id) && `security-logo-${type.id.toLowerCase()}`; return { ...type, imageId }; }); obj[id] = typesWithImages; return obj; }, {}); } else { return {}; } }); }); const runTest = async ({ testRoutine, reset = false, venueData = null }) => { if (reset || venueData) { venueDataLoaded = new Zousan(); mapDataLoaded = new Zousan(); } if (venueData) { venueDataLoaded = Zousan.resolve(venueData); } await testRoutine(); let venueDataObj, mapDataObj; if (venueDataLoaded.v) { venueDataObj = await venueDataLoaded; } if (mapDataLoaded.v) { mapDataObj = await mapDataLoaded; } return { venueDataObj, mapDataObj }; }; const init = async () => { const params = new URLSearchParams(typeof window === "undefined" ? "" : window.location.search); const lang = params.get("lang"); let languagesToTry = []; if (lang === null) { languagesToTry = typeof window === "undefined" ? [] : navigator.languages; } const deepLinkProps = config.deepLinkProps || {}; if (deepLinkProps.lang) { languagesToTry.unshift(deepLinkProps.lang); } const venueId = deepLinkProps.vid || config.venueId; const assetStage = config.useDynamicUrlParams && deepLinkProps.stage ? deepLinkProps.stage : config.assetStage; const accountId = deepLinkProps.accountId || (assetStage === "alpha" ? "A1VPTJKREFJWX5" : config.accountId); loadVenueData({ ...config, venueId, accountId, assetStage }, languagesToTry).then(notifyState); }; return { init, runTest, internal: { calculateBaseVenueId, getDefaultStructureId, getTranslatedFloorId, setConfigProperty: (key, value) => { config[key] = value; } } }; } exports.create = create;