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

244 lines (200 loc) 10 kB
'use strict'; 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;