UNPKG

geojson.lib.routeboxer

Version:

The RouteBoxer class generates an Array of GeoJSON.Polygon Objects that are guaranteed to cover every point within a specified distance of a path.

668 lines (570 loc) 22.7 kB
var geoLib = require('geojson.lib'); /** * @copyright (c) 2014 Luscus (luscus.redbeard@gmail.com) * @author luscus.redbeard@gmail.com * * @fileoverview Edited the original library to work with and output GeoJSON. */ /** * @name RouteBoxer * @version 1.0 * @copyright (c) 2010 Google Inc. * @author Thor Mitchell * * @fileoverview The RouteBoxer class takes a path, such as the Polyline for a * route generated by a Directions request, and generates a set of LatLngBounds * objects that are guaranteed to contain every point within a given distance * of that route. These LatLngBounds objects can then be used to generate * requests to spatial search services that support bounds filtering (such as * the Google Maps Data API) in order to implement search along a route. * <br/><br/> * RouteBoxer overlays a grid of the specified size on the route, identifies * every grid cell that the route passes through, and generates a set of bounds * that cover all of these cells, and their nearest neighbours. Consequently * the bounds returned will extend up to ~3x the specified distance from the * route in places. */ /* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var supportedTypes = ['MultiPoint', 'LineString']; /** * Creates a new RouteBoxer * * @constructor */ function RouteBoxer() { } module.exports = RouteBoxer; /** * Generates boxes for a given route and distance * * @param {GeoJSON.Position[] | GeoJSON.LineString} path The path along * which to create boxes. The path object can be either an * GeoJSON.Position Array, a GeoJSON.MultiPoint or GeoJSON.LineString. * @param {Number} range The distance in kms around the route that the generated * boxes must cover. * @return {GeoJSON.Polygon[]} An array of boxes that covers the whole * path. */ RouteBoxer.prototype.box = function (geojson, range) { // Two dimensional array representing the cells in the grid overlaid on the path this.grid_ = null; // Array that holds the latitude coordinate of each vertical grid line this.latGrid_ = []; // Array that holds the longitude coordinate of each horizontal grid line this.lngGrid_ = []; // Array of bounds that cover the whole route formed by merging cells that // the route intersects first horizontally, and then vertically this.boxesX_ = []; // Array of bounds that cover the whole route formed by merging cells that // the route intersects first vertically, and then horizontally this.boxesY_ = []; this.intersectingCells_ = []; this.getIntersectionPoints_ = []; // The array of LatLngs representing the vertices of the path var vertices = null; // If necessary convert the path into an array of LatLng objects if (geojson instanceof Array) { // already an array of Positions: [[x1,y1,z1], ..., [xn,yn,zn]] vertices = geojson; } else if (typeof geojson === 'object' && supportedTypes.indexOf(geojson.type) > -1 ) { vertices = geojson.coordinates; } else { throw new Error( 'Boxer awaits an Array of Positions (Position: [x/lon, y/lat, z/alt]) ' + 'or following GeoJSON types providing a Position Array: ' + supportedTypes ); } // Build the grid that is overlaid on the route this.buildGrid_(vertices, range); // Identify the grid cells that the route intersects this.findIntersectingCells_(vertices); // Merge adjacent intersected grid cells (and their neighbours) into two sets // of bounds, both of which cover them completely this.mergeIntersectingCells_(); // Return the set of merged bounds that has the fewest elements return this.getPolygonsFromBounds_(this.boxesX_.length <= this.boxesY_.length ? this.boxesX_ : this.boxesY_); }; /** * Generates boxes for a given route and distance * * @param {GeoJSON.Position[]} vertices The vertices of the path over which to lay the grid * @param {Number} range The spacing of the grid cells. */ RouteBoxer.prototype.buildGrid_ = function (vertices, range) { // Create a LatLngBounds object that contains the whole path var routeBounds = geoLib.tools.getBBoxFromVertices(vertices); // Find the center of the bounding box of the path var routeBoundsCenter = geoLib.tools.getBBoxCenter(routeBounds); // Starting from the center define grid lines outwards vertically until they // extend beyond the edge of the bounding box by more than one cell this.latGrid_.push(routeBoundsCenter.coordinates[1]); // Add lines from the center out to the north this.latGrid_.push(geoLib.tools.destinationPosition(routeBoundsCenter, 0, range)[1]); for (i = 2; this.latGrid_[i - 2] < routeBounds[3]; i++) { this.latGrid_.push(geoLib.tools.destinationPosition(routeBoundsCenter, 0, range * i)[1]); } // Add lines from the center out to the south for (i = 1; this.latGrid_[1] > routeBounds[1]; i++) { this.latGrid_.unshift(geoLib.tools.destinationPosition(routeBoundsCenter, 180, range * i)[1]); } // Starting from the center define grid lines outwards horizontally until they // extend beyond the edge of the bounding box by more than one cell this.lngGrid_.push(routeBoundsCenter.coordinates[0]); // Add lines from the center out to the east this.lngGrid_.push(geoLib.tools.destinationPosition(routeBoundsCenter, 90, range)[0]); for (i = 2; this.lngGrid_[i - 2] < routeBounds[2]; i++) { this.lngGrid_.push(geoLib.tools.destinationPosition(routeBoundsCenter, 90, range * i)[0]); } // Add lines from the center out to the west for (i = 1; this.lngGrid_[1] > routeBounds[0]; i++) { this.lngGrid_.unshift(geoLib.tools.destinationPosition(routeBoundsCenter, 270, range * i)[0]); } // fill the grid rows with "0" var xArray = []; for (x = 0; x < this.latGrid_.length; x++) { xArray.push(0); } // Create a two dimensional array representing this grid this.grid_ = new Array(this.lngGrid_.length); for (i = 0; i < this.grid_.length; i++) { this.grid_[i] = [].concat(xArray); } // setting the grid processing boundaries this.lngGridBoundary_ = this.lngGrid_.length - 2; this.latGridBoundary_ = this.latGrid_.length - 2; }; /** * Find all of the cells in the overlaid grid that the path intersects * * @param {LatLng[]} vertices The vertices of the path */ RouteBoxer.prototype.findIntersectingCells_ = function (vertices) { // Find the cell where the path begins var hintXY = this.getCellCoords_(vertices[0]); // Mark that cell and it's neighbours for inclusion in the boxes this.markCell_(hintXY); // Work through each vertex on the path identifying which grid cell it is in for (var i = 1; i < vertices.length; i++) { // Use the known cell of the previous vertex to help find the cell of this vertex var gridXY = this.getGridCoordsFromHint_(vertices[i], vertices[i - 1], hintXY); if (gridXY[0] === hintXY[0] && gridXY[1] === hintXY[1]) { // This vertex is in the same cell as the previous vertex // The cell will already have been marked for inclusion in the boxes continue; } else if ((Math.abs(hintXY[0] - gridXY[0]) === 1 && hintXY[1] === gridXY[1]) || (hintXY[0] === gridXY[0] && Math.abs(hintXY[1] - gridXY[1]) === 1)) { // This vertex is in a cell that shares an edge with the previous cell // Mark this cell and it's neighbours for inclusion in the boxes this.markCell_(gridXY); } else { // This vertex is in a cell that does not share an edge with the previous // cell. This means that the path passes through other cells between // this vertex and the previous vertex, and we must determine which cells // it passes through this.getGridIntersects_(vertices[i - 1], vertices[i], hintXY, gridXY); } // Use this cell to find and compare with the next one hintXY = gridXY; } }; /** * Find the cell a path vertex is in by brute force iteration over the grid * * @param {GeoJSON.Position} latlng The latlng of the vertex * @return {Number[][]} The cell coordinates of this vertex in the grid */ RouteBoxer.prototype.getCellCoords_ = function (Position) { var x, y; for (x = 0; this.lngGrid_[x] < Position[0]; x++) {} for (y = 0; this.latGrid_[y] < Position[1]; y++) {} return ([x - 1, y - 1]); }; /** * Find the cell a path vertex is in based on the known location of a nearby * vertex. This saves searching the whole grid when working through vertices * on the polyline that are likely to be in close proximity to each other. * * @param {GeoJSON.Position} latlng The latlng of the vertex to locate in the grid * @param {GeoJSON.Position} hintlatlng The latlng of the vertex with a known location * @param {Number[]} hint The cell containing the vertex with a known location * @return {Number[]} The cell coordinates of the vertex to locate in the grid */ RouteBoxer.prototype.getGridCoordsFromHint_ = function (latlng, hintlatlng, hint) { var x, y; if (latlng[0] > hintlatlng[0]) { for (x = hint[0]; this.lngGrid_[x + 1] < latlng[0]; x++) {} } else { for (x = hint[0]; this.lngGrid_[x] > latlng[0]; x--) {} } if (latlng[1] > hintlatlng[1]) { for (y = hint[1]; this.latGrid_[y + 1] < latlng[1]; y++) {} } else { for (y = hint[1]; this.latGrid_[y] > latlng[1]; y--) {} } return this.enforceGridProcessingBoundaries([x, y]); }; /** * The cell found from hints have to be within the processing * boundaries - that is it can't be any of the outmost cells of * the grid. Those will be chosed by calculating the nabours * * @param {Number[]} cell * @returns {Number[]} cell within the boundaries */ RouteBoxer.prototype.enforceGridProcessingBoundaries = function (cell) { if (cell[0] < 1) { cell[0] = 1; } else if (this.lngGridBoundary_ < cell[0]) { cell[0] = this.lngGridBoundary_; } if (cell[1] < 1) { cell[1] = 1; } else if (this.latGridBoundary_ < cell[1]) { cell[1] = this.latGridBoundary_; } return cell; }; /** * Identify the grid squares that a path segment between two vertices * intersects with by: * 1. Finding the bearing between the start and end of the segment * 2. Using the delta between the lat of the start and the lat of each * latGrid boundary to find the distance to each latGrid boundary * 3. Finding the lng of the intersection of the line with each latGrid * boundary using the distance to the intersection and bearing of the line * 4. Determining the x-coord on the grid of the point of intersection * 5. Filling in all squares between the x-coord of the previous intersection * (or start) and the current one (or end) at the current y coordinate, * which is known for the grid line being intersected * * @param {GeoJSON.Position} start The latlng of the vertex at the start of the segment * @param {GeoJSON.Position} end The latlng of the vertex at the end of the segment * @param {Number[]} startXY The cell containing the start vertex * @param {Number[]} endXY The cell containing the vend vertex */ RouteBoxer.prototype.getGridIntersects_ = function (start, end, startXY, endXY) { var gridStart, edgePoint, edgeXY, i; var brng = geoLib.tools.rhumbBearingTo(start, end, 4); // Step 1. var gridBearing = this.getGridVectorBearings_(brng); var gridVectorStart = this.getGridVectorStart_(startXY, endXY); var hint = start; var hintXY = startXY; // Handle a line segment that travels south first if (end[1] > start[1]) { // Iterate over the east to west grid lines between the start and end cells for (i = startXY[1] + 1; i <= endXY[1]; i++) { // Find the latlng of the point where the path segment intersects with // this grid line (Step 2 & 3) gridStart = [this.lngGrid_[gridVectorStart[0]], this.latGrid_[i]]; edgePoint = geoLib.tools.intersectionPosition(start, brng, gridStart, gridBearing[1]); this.getIntersectionPoints_.push({type: 'Point', coordinates: edgePoint}); // Find the cell containing this intersect point (Step 4) edgeXY = this.getGridCoordsFromHint_(edgePoint, hint, hintXY); // Mark every cell the path has crossed between this grid and the start, // or the previous east to west grid line it crossed (Step 5) this.fillInGridSquares_(hintXY[0], edgeXY[0], i - 1); // Use the point where it crossed this grid line as the reference for the // next iteration hint = edgePoint; hintXY = edgeXY; } // Mark every cell the path has crossed between the last east to west grid // line it crossed and the end (Step 5) this.fillInGridSquares_(hintXY[0], endXY[0], i - 1); } else { // Iterate over the east to west grid lines between the start and end cells for (i = startXY[1]; i > endXY[1]; i--) { // Find the latlng of the point where the path segment intersects with // this grid line (Step 2 & 3) gridStart = [this.lngGrid_[gridVectorStart[0]], this.latGrid_[i]]; edgePoint = geoLib.tools.intersectionPosition(start, brng, gridStart, gridBearing[1]); this.getIntersectionPoints_.push({type: 'Point', coordinates: edgePoint}); // Find the cell containing this intersect point (Step 4) edgeXY = this.getGridCoordsFromHint_(edgePoint, hint, hintXY); // Mark every cell the path has crossed between this grid and the start, // or the previous east to west grid line it crossed (Step 5) this.fillInGridSquares_(hintXY[0], edgeXY[0], i); // Use the point where it crossed this grid line as the reference for the // next iteration hint = edgePoint; hintXY = edgeXY; } // Mark every cell the path has crossed between the last east to west grid // line it crossed and the end (Step 5) this.fillInGridSquares_(hintXY[0], endXY[0], i); } }; RouteBoxer.prototype.getGridVectorStart_ = function (startCell, endCell) { var start = []; // bearing for longitude line if (startCell[0] < endCell[0]) { start.push(startCell[0]); } else { start.push(startCell[0] + 1); } // bearing for latitude line if (startCell[1] < endCell[1]) { start.push(startCell[1]); } else { start.push(startCell[1] + 1); } return start }; RouteBoxer.prototype.getGridVectorBearings_ = function (bearing) { var bearings = []; // bearing for longitude line if (90 < bearing && bearing < 270) { bearings.push(180); } else { bearings.push(0); } // bearing for latitude line if (0 < bearing && bearing < 180) { bearings.push(90); } else { bearings.push(270); } return bearings }; /** * Mark all cells in a given row of the grid that lie between two columns * for inclusion in the boxes * * @param {Number} startx The first column to include * @param {Number} endx The last column to include * @param {Number} y The row of the cells to include */ RouteBoxer.prototype.fillInGridSquares_ = function (startx, endx, y) { var x; if (startx < endx) { for (x = startx; x <= endx; x++) { this.markCell_([x, y]); } } else { for (x = startx; x >= endx; x--) { this.markCell_([x, y]); } } }; /** * Mark a cell and the 8 immediate neighbours for inclusion in the boxes * * @param {Number[]} square The cell to mark */ RouteBoxer.prototype.markCell_ = function (cell) { var x = cell[0]; var y = cell[1]; this.intersectingCells_.push(cell); this.grid_[x - 1][y - 1] = 1; this.grid_[x][y - 1] = 1; this.grid_[x + 1][y - 1] = 1; this.grid_[x - 1][y] = 1; this.grid_[x][y] = 1; this.grid_[x + 1][y] = 1; this.grid_[x - 1][y + 1] = 1; this.grid_[x][y + 1] = 1; this.grid_[x + 1][y + 1] = 1; }; /** * Create two sets of bounding boxes, both of which cover all of the cells that * have been marked for inclusion. * * The first set is created by combining adjacent cells in the same column into * a set of vertical rectangular boxes, and then combining boxes of the same * height that are adjacent horizontally. * * The second set is created by combining adjacent cells in the same row into * a set of horizontal rectangular boxes, and then combining boxes of the same * width that are adjacent vertically. * */ RouteBoxer.prototype.mergeIntersectingCells_ = function () { var x, y, box; // The box we are currently expanding with new cells var currentBox = null; // Traverse the grid a row at a time for (y = 0; y < this.grid_[0].length; y++) { for (x = 0; x < this.grid_.length; x++) { if (this.grid_[x][y]) { // This cell is marked for inclusion. If the previous cell in this // row was also marked for inclusion, merge this cell into it's box. // Otherwise start a new box. box = this.getCellBounds_([x, y]); if (currentBox) { geoLib.tools.extendBBoxWithPosition(currentBox, [box[2], box[3]]); } else { currentBox = box; } } else { // This cell is not marked for inclusion. If the previous cell was // marked for inclusion, merge it's box with a box that spans the same // columns from the row below if possible. this.mergeBoxesY_(currentBox); currentBox = null; } } // If the last cell was marked for inclusion, merge it's box with a matching // box from the row below if possible. this.mergeBoxesY_(currentBox); currentBox = null; } // Traverse the grid a column at a time for (x = 0; x < this.grid_.length; x++) { for (y = 0; y < this.grid_[0].length; y++) { if (this.grid_[x][y]) { // This cell is marked for inclusion. If the previous cell in this // column was also marked for inclusion, merge this cell into it's box. // Otherwise start a new box. if (currentBox) { box = this.getCellBounds_([x, y]); geoLib.tools.extendBBoxWithPosition(currentBox, [box[2], box[3]]); } else { currentBox = this.getCellBounds_([x, y]); } } else { // This cell is not marked for inclusion. If the previous cell was // marked for inclusion, merge it's box with a box that spans the same // rows from the column to the left if possible. this.mergeBoxesX_(currentBox); currentBox = null; } } // If the last cell was marked for inclusion, merge it's box with a matching // box from the column to the left if possible. this.mergeBoxesX_(currentBox); currentBox = null; } }; /** * Search for an existing box in an adjacent row to the given box that spans the * same set of columns and if one is found merge the given box into it. If one * is not found, append this box to the list of existing boxes. * * @param {LatLngBounds} The box to merge */ RouteBoxer.prototype.mergeBoxesX_ = function (box) { if (box !== null) { for (var i = 0; i < this.boxesX_.length; i++) { if (this.boxesX_[i][2] === box[0] && this.boxesX_[i][1] === box[1] && this.boxesX_[i][3] === box[3]) { geoLib.tools.extendBBoxWithPosition(this.boxesX_[i], [box[2], box[3]]); return; } } this.boxesX_.push(box); } }; /** * Search for an existing box in an adjacent column to the given box that spans * the same set of rows and if one is found merge the given box into it. If one * is not found, append this box to the list of existing boxes. * * @param {LatLngBounds} The box to merge */ RouteBoxer.prototype.mergeBoxesY_ = function (box) { if (box !== null) { for (var i = 0; i < this.boxesY_.length; i++) { if (this.boxesY_[i][3] === box[1] && this.boxesY_[i][0] === box[0] && this.boxesY_[i][2] === box[2]) { geoLib.tools.extendBBoxWithPosition(this.boxesY_[i], [box[2], box[3]]); return; } } this.boxesY_.push(box); } }; /** * Obtain the LatLng of the origin of a cell on the grid * * @param {Number[]} cell The cell to lookup. * @return {LatLng} The latlng of the origin of the cell. */ RouteBoxer.prototype.getCellBounds_ = function (cell) { return geoLib.tools.getBBoxFromVertices( [[this.lngGrid_[cell[0]], this.latGrid_[cell[1]]], [this.lngGrid_[cell[0] + 1], this.latGrid_[cell[1] + 1]]]); }; /** * Obtain an Array of GeoJSON.Polygons from an Array of Bounding Boxes * * @param {bbox[]} Array of Bounding Boxes. * @return {GeoJSON.Polygon[]} Array of GeoJSON.Polygon. */ RouteBoxer.prototype.getPolygonsFromBounds_ = function (bboxs) { var polygons = []; bboxs.forEach(function bboxIterator(bbox) { polygons.push(geoLib.tools.bboxToPolygon(bbox)); }); return polygons; }; /** * Obtain an Array of GeoJSON.Polygons from an Array of Bounding Boxes * * @return {GeoJSON.MultiLineString} one GeoJSON.MultiLineString representing the grid. */ RouteBoxer.prototype.getGrid = function () { var self = this; var xMax = this.lngGrid_.length - 1; var yMax = this.latGrid_.length - 1; var lines = []; this.lngGrid_.forEach(function (x) { lines.push([ [x, self.latGrid_[0]], [x, self.latGrid_[yMax]] ]); }); this.latGrid_.forEach(function (y) { lines.push([ [self.lngGrid_[0], y], [self.lngGrid_[xMax], y] ]); }); return { type: 'MultiLineString', bbox: [ this.lngGrid_[0], this.latGrid_[0], this.lngGrid_[xMax], this.latGrid_[yMax] ], coordinates: lines }; }; RouteBoxer.prototype.getIntersectingCells = function () { var self = this; var polygons = []; this.intersectingCells_.forEach(function (cell) { var index = polygons.push([[ [self.lngGrid_[cell[0] +1], self.latGrid_[cell[1] + 1]], [self.lngGrid_[cell[0]], self.latGrid_[cell[1] + 1]], [self.lngGrid_[cell[0]], self.latGrid_[cell[1]]], [self.lngGrid_[cell[0] + 1], self.latGrid_[cell[1]]], [self.lngGrid_[cell[0] + 1], self.latGrid_[cell[1] + 1]] ]]); }); return { type: 'MultiPolygon', coordinates: polygons }; }; RouteBoxer.prototype.getIntersectionPoints = function () { return this.getIntersectionPoints_; };