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
JavaScript
;
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;