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