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
244 lines (200 loc) • 10 kB
JavaScript
;
var area = require('@turf/area');
var bboxClip = require('@turf/bbox-clip');
var bboxPolygon = require('@turf/bbox-polygon');
var helpers = require('@turf/helpers');
require('@turf/point-to-line-distance');
var R = require('ramda');
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);
// https://stackoverflow.com/questions/22521982/check-if-point-inside-a-polygon
const pointInPolygon = (point, vs) => {
const x = point[0];
const y = point[1];
let inside = false;
for (let i = 0, j = vs.length - 1; i < vs.length; j = i++) {
const xi = vs[i][0];
const yi = vs[i][1];
const xj = vs[j][0];
const yj = vs[j][1];
const intersect = ((yi > y) !== (yj > y)) &&
(x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) {
inside = !inside;
}
}
return inside
};
const bounds2Coords = ({ n, s, e, w }) => [[n, e], [n, w], [s, w], [s, e], [n, e]];
const NULL_STRUCTURE_AND_FLOOR = { structure: null, floor: null };
/**
* Pass in a structures array and a lat,lng and ord, and this will return the structure and floor
* at that point or nulls if no structure exists at that point/ord. This runs very quickly as we very often
* can skip the slowest path (which runs pointInPolygon). Without preciseFlag we sometimes return
* a structure when the point falls outside it - but won't return a wrong structure. In some cases this
* is actually desired behavior anyway (such as structure selector)
* @param {object} structures the venue data structures
* @param {float} lat latitude of point to check
* @param {float} lng longitude of point to check
* @param {[float]} mapviewBBox the minx,miny,maxx,maxy lat lng values that define the viewable map (not req if preciseFlag === true)
* @param {boolean} preciseFlag if false, we provide a fuzzy reading, allowing for the map center to be "close enough" to a building/floor
* @returns {structure: object, floor: object} structure/building and floor that contains the point passed (or {structure:null,floor:null})
*/
function getStructureAndFloorAtPoint (structures, lat, lng, ord, mapviewBBox, preciseFlag = false) { // Step 1 of logic flow
if (!R__namespace.length(structures)) return NULL_STRUCTURE_AND_FLOOR
structures = structures.filter(s => s.shouldDisplay == null || s.shouldDisplay === true);
const floorsToConsider = structures
.map(structure => ({ structure, floor: ordToFloor(structure, ord) })); // array of {structure,floor} obs
return getStructureAndFloorWithinFloorsAtPoint(structures, floorsToConsider, lat, lng, mapviewBBox, preciseFlag)
}
/**
* Given a list of candidate floors, and a lat,lng point, determine which
* @param {object} structures the venue data structures
* @param {[{structure, floor}]} floorsToConsider A list of "tuple" objects containing structure and floor (both can be null)
* @param {float} lat latitude of point to check
* @param {float} lng longitude of point to check
* @param {[float]} mapviewBBox the minx,miny,maxx,maxy lat lng values that define the viewable map (not req if preciseFlag === true)
* @param {boolean} preciseFlag if false, we provide a fuzzy reading, allowing for the map center to be "close enough" to a building/floor
* @returns {structure: object, floor: object} structure/building and floor that contains the point passed (or {structure:null,floor:null})
*/
function getStructureAndFloorWithinFloorsAtPoint (structures, floorsToConsider, lat, lng, mapviewBBox, preciseFlag) {
// Step 2 - Select floors whose bounding box contains map center
const pointWithinFloorsBBox = floorsToConsider
.filter(ftc => ftc.floor) // ignore structures with no floor on this ord
.filter(ftc => pointInPolygon([lat, lng], bounds2Coords(ftc.floor.bounds)));
//
// First, lets handle the simpler case, where preciseFlag is true.
// All preciseFlag=true cases are handled within this code block.
//
if (preciseFlag) {
// not within any floor's bounding box? return nulls
if (pointWithinFloorsBBox.length === 0)
return NULL_STRUCTURE_AND_FLOOR
// Step 3 (precise) - We need to determine which of the floors found above are we ACTUALLY in:
const floorsWithinBoundsPolygon = pointWithinFloorsBBox
.filter(ftc => pointInPolygon([lat, lng], ftc.floor.boundsPolygon));
// We should never be in MORE than one floor's bounding polygon, so return 1st one
// and in unlikely case we ARE in multiple, user will get first one...
if (floorsWithinBoundsPolygon.length >= 1)
return R__namespace.head(floorsWithinBoundsPolygon)
// precise yet not within any floor polygon, "so you get nothing. you lose. good day sir!"
return NULL_STRUCTURE_AND_FLOOR
}
//
// From here forward, we handle the non-precise case (more complicated)
//
// Step 3 (non-precise) - We are not within any *floor* bounding box..
if (pointWithinFloorsBBox.length === 0) {
// Check to see if we are over a building (perhaps with no floor or tiny floor at this ordinal)
const floorsWithinBuildingBoundingBox = structures
.filter(structure => pointInPolygon([lat, lng], bounds2Coords(structure.bounds)))
// .map(structure => ({ structure, floor: structure.levels[structure.defaultLevelId] }))
.map(structure => ({ structure, floor: null }));
if (floorsWithinBuildingBoundingBox.length >= 1)
return floorsWithinBuildingBoundingBox[0]
return NULL_STRUCTURE_AND_FLOOR // user does not seem to be near ANYTHING!
}
// Step 4 - If we are only in the bounding box of a single floor, return it
if (pointWithinFloorsBBox.length === 1)
return pointWithinFloorsBBox[0]
// Step 5 - Ok, so from here, we are NOT precise, and the map center is within MULTIPLE bounding boxes
// so how do we determine WHICH item to select...?
const floorsContainingPoint = pointWithinFloorsBBox.filter(ftc => pointInPolygon([lat, lng], ftc.floor.boundsPolygon));
// We will score the building/floor's "prominence" and pick the highest scoring building/floor
const prominenceScores = pointWithinFloorsBBox.map(ftc => prominence(ftc, mapviewBBox, floorsContainingPoint.some(fcp => fcp.floor.id === ftc.floor.id)));
const bestScore = Math.max.apply(null, prominenceScores);
return pointWithinFloorsBBox[prominenceScores.indexOf(bestScore)]
}
// Returns a prominenceScore from 0 to 100
// This is calculated by a % of screen taken by the
// floor polygon
function prominence ({ structure, floor }, mapviewBBox, pointWithinFloorPoly) {
// Take the polygon of the floor...
const floorPolygon = coords2Poly(floor.boundsPolygon);
// ...and the bounding box of the viewable map...
const mapviewBBoxPoly = bboxPolygon.bboxPolygon(mapviewBBox);
// ...and create a viewable floor polygone from the intercection.
const viewableFloorPoly = bboxClip.bboxClip(floorPolygon, mapviewBBox);
const floorArea = area.area(viewableFloorPoly);
const viewableMapArea = area.area(mapviewBBoxPoly);
// now the prominence is simply the ratio of viewable floor to viewable map (with 20% bonus if center within floor)
return floorArea * (pointWithinFloorPoly ? 150 : 100) / viewableMapArea
}
const latLngSwap = point => [point[1], point[0]];
const coords2Poly = coords => helpers.polygon([coords.map(latLngSwap)]);
/**
* given a building and ord, return the floor (or undefined if doesn't exist)
*/
const ordToFloor = (building, ord) =>
Object.values(building.levels).find(floor => floor.ordinal === ord);
/**
* Return the floor based on its ID (pass in buildings array and floorId)
*/
const getFloor = (structures, selectedLevelId) =>
structures.reduce((fmatch, building) =>
Object.values(building.levels).find(floor => floor.id === selectedLevelId) || fmatch, undefined);
// pass in the structures array and a floorId and this will return the structure
// that contains the floorId.
const getStructureForFloorId = (structures, floorId) =>
structures.reduce((sMatch, structure) =>
buildContainsFloorWithId(structure, floorId) ? structure : sMatch, null);
// returns true if the building specified contains the floorId specified
const buildContainsFloorWithId = (building, floorId) =>
Object.values(building.levels).reduce((fmatch, floor) =>
floor.id === floorId ? true : fmatch, false);
/**
* Calculate the points for a bezier cubic curve
*
* @param {number} fromX - Starting point x
* @param {number} fromY - Starting point y
* @param {number} cpX - Control point x
* @param {number} cpY - Control point y
* @param {number} cpX2 - Second Control point x
* @param {number} cpY2 - Second Control point y
* @param {number} toX - Destination point x
* @param {number} toY - Destination point y
* @return {Object[]} Array of points of the curve
*/
function bezierCurveTo (fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) {
const n = 20; // controls smoothness of line
let dt = 0;
let dt2 = 0;
let dt3 = 0;
let t2 = 0;
let t3 = 0;
const path = [{ x: fromX, y: fromY }];
for (let i = 1, j = 0; i <= n; ++i) {
j = i / n;
dt = (1 - j);
dt2 = dt * dt;
dt3 = dt2 * dt;
t2 = j * j;
t3 = t2 * j;
path.push({
x: (dt3 * fromX) + (3 * dt2 * j * cpX) + (3 * dt * t2 * cpX2) + (t3 * toX),
y: (dt3 * fromY) + (3 * dt2 * j * cpY) + (3 * dt * t2 * cpY2) + (t3 * toY)
});
}
return path
}
exports.bezierCurveTo = bezierCurveTo;
exports.getFloor = getFloor;
exports.getStructureAndFloorAtPoint = getStructureAndFloorAtPoint;
exports.getStructureForFloorId = getStructureForFloorId;
exports.ordToFloor = ordToFloor;
exports.pointInPolygon = pointInPolygon;