├── .gitignore ├── bower.json ├── package.json ├── example ├── css │ └── m.css ├── index.html ├── js │ └── app.js └── lib │ └── Polyline.encoded.js ├── README.md ├── dist └── leaflet-routeboxer.min.js └── src └── leaflet-routeboxer.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet-routeboxer", 3 | "homepage": "https://github.com/StephanGeorg/leaflet-routeboxer", 4 | "authors": [ 5 | "Stephan Georg" 6 | ], 7 | "description": "A Leaflet implementation of Google's RouteBoxer. RouteBoxer generates a set of L.LatLngBounds objects that are guaranteed to cover every point within a specified distance of a path.", 8 | "main": "dist/leaflet-routeboxer.min.js", 9 | "moduleType": [ 10 | "globals" 11 | ], 12 | "keywords": [ 13 | "routeboxer", 14 | "leaflet" 15 | ], 16 | "ignore": [ 17 | "**/.*", 18 | "node_modules", 19 | "bower_components", 20 | "test", 21 | "tests" 22 | ], 23 | "dependencies": { 24 | "leaflet": "~0.7.7" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet-routeboxer", 3 | "version": "1.0.4", 4 | "description": "Google RouteBoxer implementation for Leaflet", 5 | "main": "./src/leaflet-routeboxer.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/StephanGeorg/leaflet-routeboxer.git" 15 | }, 16 | "keywords": [ 17 | "maps", 18 | "leaflet", 19 | "routeboxer" 20 | ], 21 | "author": "Stephan Georg", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/StephanGeorg/leaflet-routeboxer/issues" 25 | }, 26 | "homepage": "https://github.com/StephanGeorg/leaflet-routeboxer#readme", 27 | "dependencies": { 28 | "leaflet": "^0.7.7" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/css/m.css: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100%; 3 | width: 100%; 4 | padding: 0; 5 | margin: 0; 6 | font: 16px "Source Sans", helvetica, arial, sans-serif; 7 | font-weight: 400; 8 | } 9 | #map { 10 | height:100%; 11 | width: 100%; 12 | position: absolute; 13 | } 14 | #controls { 15 | display: block; 16 | position: absolute; 17 | top: 10px; 18 | right: 10px; 19 | background: #fff; 20 | box-shadow: 0 1px 5px rgba(0,0,0,0.65); 21 | border-radius: 4px; 22 | width: 300px; 23 | z-index: 10000; 24 | } 25 | #controls .wrapper { 26 | padding: 30px; 27 | } 28 | 29 | #result { 30 | list-style: none; 31 | margin: 0; 32 | padding: 0; 33 | } 34 | 35 | @media (max-width: 720px) { 36 | h2 { 37 | margin: 0; 38 | } 39 | 40 | #controls { 41 | top: inherit; 42 | right: inherit; 43 | bottom: 10px; 44 | width: 95%; 45 | margin: 0 2.5%; 46 | } 47 | 48 | #controls .wrapper { 49 | padding: 10px; 50 | } 51 | 52 | 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Leaflet RouteBoxer 2 | 3 | This is a Leaflet implementation of Google's RouteBoxer class. 4 | 5 | The RouteBoxer class generates a set of L.LatLngBounds objects that are guaranteed 6 | to cover every point within a specified distance of a path, such as that generated 7 | for a route by an OSRM directions service. 8 | 9 | ## Example 10 | 11 | Check out the example [demo](https://stephangeorg.github.io/leaflet-routeboxer/example/) 12 | 13 | ## Usage 14 | 15 | You need to pass an array of L.Latlng objects (route) to the L.RouteBoxer. 16 | 17 | 18 | ```javascript 19 | 20 | var route = [ 21 | [50.5, 30.5], 22 | [50.4, 30.6], 23 | [50.3, 30.7] 24 | ]; 25 | var distance = 10; // Distance in km 26 | var boxes = L.RouteBoxer.box(route, distance); 27 | 28 | ``` 29 | 30 | ### Using w/ OSRM service 31 | 32 | OSRM uses polyline encoding to save bandwith. To decode the polyline you can use 33 | [Leaflet.encoded](https://github.com/jieter/Leaflet.encoded). 34 | 35 | ```javascript 36 | 37 | // data.route_geometry is the result from a OSRM endpoint 38 | var route = new L.Polyline(L.PolylineUtil.decode(data.route[0].geometry)); 39 | var distance = 10; // Distance in km 40 | var boxes = L.RouteBoxer.box(route, distance); 41 | 42 | ``` 43 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet-RouterBox plugin example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 |
38 |

Leaflet RouteBoxer

39 | 40 |

41 | The RouteBoxer class generates a set of L.LatLngBounds objects that are 42 | guaranteed to cover every point within a specified distance of a path, 43 | such as that generated for a route by an OSRM directions service.

44 | Documentation
45 | powered by Nearest! 46 |

47 | 48 | 49 | Star 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /example/js/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example app to show leaflet-routerbox 3 | * 4 | * 5 | **/ 6 | function App() { 7 | 8 | this.route = []; 9 | this.map = L.map('map').setView([50.8453995, 10.556859500000003],6); 10 | this.bounds = {}; 11 | this.distance = 10; // Distance in km 12 | 13 | var layer = L.tileLayer('http://osm.nearest.place/retina/{z}/{x}/{y}.png', { 14 | attribution: '© OpenStreetMap contributors, Tiles © Distance.to' 15 | }).addTo(this.map); 16 | 17 | // Waypoints for getting a route of 18 | var loc = [ 19 | '9.992196,53.553406', 20 | '11.580186,48.139126' 21 | ]; 22 | 23 | this.route = this.loadRoute(loc, this.drawRoute); 24 | 25 | } 26 | 27 | /** 28 | * Format an array of LatLng for L.polyline from uncompressed OSRM request 29 | * 30 | */ 31 | App.prototype.formArray = function (arr) { 32 | var narr = []; 33 | for(var x=0;xr.getSouthWest().lat;s++)this.latGrid_.unshift(e.rhumbDestinationPoint(180,i*s).lat);for(this.lngGrid_.push(e.lng),this.lngGrid_.push(e.rhumbDestinationPoint(90,i).lng),s=2;this.lngGrid_[s-2]r.getSouthWest().lng;s++)this.lngGrid_.unshift(e.rhumbDestinationPoint(270,i*s).lng);for(this.grid_=new Array(this.lngGrid_.length),s=0;si.lng)for(s=r[0];this.lngGrid_[s+1]t.lng;s--);if(t.lat>i.lat)for(e=r[1];this.latGrid_[e+1]t.lat;e--);return[s,e]},getGridIntersects_:function(t,i,r,s){var e,n,h,o=t.rhumbBearingTo(i),a=t,l=r;if(i.lat>t.lat){for(h=r[1]+1;h<=s[1];h++)e=this.getGridIntersect_(t,o,this.latGrid_[h]),n=this.getGridCoordsFromHint_(e,a,l),this.fillInGridSquares_(l[0],n[0],h-1),a=e,l=n;this.fillInGridSquares_(l[0],s[0],h-1)}else{for(h=r[1];h>s[1];h--)e=this.getGridIntersect_(t,o,this.latGrid_[h]),n=this.getGridCoordsFromHint_(e,a,l),this.fillInGridSquares_(l[0],n[0],h),a=e,l=n;this.fillInGridSquares_(l[0],s[0],h)}},getGridIntersect_:function(t,i,r){var s=this.R*((r.toRad()-t.lat.toRad())/Math.cos(i.toRad()));return t.rhumbDestinationPoint(i,s)},fillInGridSquares_:function(t,i,r){var s;if(i>t)for(s=t;i>=s;s++)this.markCell_([s,r]);else for(s=t;s>=i;s--)this.markCell_([s,r])},markCell_:function(t){var i=t[0],r=t[1];this.grid_[i-1][r-1]=1,this.grid_[i][r-1]=1,this.grid_[i+1][r-1]=1,this.grid_[i-1][r]=1,this.grid_[i][r]=1,this.grid_[i+1][r]=1,this.grid_[i-1][r+1]=1,this.grid_[i][r+1]=1,this.grid_[i+1][r+1]=1},mergeIntersectingCells_:function(){var t,i,r,s=null;for(i=0;i1e-10?o/a:Math.cos(e),g=s*Math.sin(t)/l;Math.abs(h)>Math.PI/2&&(h=h>0?Math.PI-h:-(Math.PI-h));var _=(n+g+Math.PI)%(2*Math.PI)-Math.PI;return isNaN(h)||isNaN(_)?null:new L.LatLng(h.toDeg(),_.toDeg())},L.LatLng.prototype.rhumbBearingTo=function(t){var i=(t.lng-this.lng).toRad(),r=Math.log(Math.tan(t.lat.toRad()/2+Math.PI/4)/Math.tan(this.lat.toRad()/2+Math.PI/4));return Math.abs(i)>Math.PI&&(i=i>0?-(2*Math.PI-i):2*Math.PI+i),Math.atan2(i,r).toBrng()},Number.prototype.toRad=function(){return this*Math.PI/180},Number.prototype.toDeg=function(){return 180*this/Math.PI},Number.prototype.toBrng=function(){return(this.toDeg()+360)%360}; 2 | -------------------------------------------------------------------------------- /example/lib/Polyline.encoded.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Utility functions to decode/encode numbers and array's of numbers 3 | * to/from strings (Google maps polyline encoding) 4 | * 5 | * Extends the L.Polyline and L.Polygon object with methods to convert 6 | * to and create from these strings. 7 | * 8 | * Jan Pieter Waagmeester 9 | * 10 | * Original code from: 11 | * http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/ 12 | * (which is down as of december 2014) 13 | */ 14 | 15 | (function () { 16 | 'use strict'; 17 | 18 | var defaultOptions = function (options) { 19 | if (typeof options === 'number') { 20 | // Legacy 21 | options = { 22 | precision: options 23 | }; 24 | } else { 25 | options = options || {}; 26 | } 27 | 28 | options.precision = options.precision || 5; 29 | options.factor = options.factor || Math.pow(10, options.precision); 30 | options.dimension = options.dimension || 2; 31 | return options; 32 | }; 33 | 34 | var PolylineUtil = { 35 | encode: function (points, options) { 36 | options = defaultOptions(options); 37 | 38 | var flatPoints = []; 39 | for (var i = 0, len = points.length; i < len; ++i) { 40 | var point = points[i]; 41 | 42 | if (options.dimension === 2) { 43 | flatPoints.push(point.lat || point[0]); 44 | flatPoints.push(point.lng || point[1]); 45 | } else { 46 | for (var dim = 0; dim < options.dimension; ++dim) { 47 | flatPoints.push(point[dim]); 48 | } 49 | } 50 | } 51 | 52 | return this.encodeDeltas(flatPoints, options); 53 | }, 54 | 55 | decode: function (encoded, options) { 56 | options = defaultOptions(options); 57 | 58 | var flatPoints = this.decodeDeltas(encoded, options); 59 | 60 | var points = []; 61 | for (var i = 0, len = flatPoints.length; i + (options.dimension - 1) < len;) { 62 | var point = []; 63 | 64 | for (var dim = 0; dim < options.dimension; ++dim) { 65 | point.push(flatPoints[i++]); 66 | } 67 | 68 | points.push(point); 69 | } 70 | 71 | return points; 72 | }, 73 | 74 | encodeDeltas: function(numbers, options) { 75 | options = defaultOptions(options); 76 | 77 | var lastNumbers = []; 78 | 79 | for (var i = 0, len = numbers.length; i < len;) { 80 | for (var d = 0; d < options.dimension; ++d, ++i) { 81 | var num = numbers[i]; 82 | var delta = num - (lastNumbers[d] || 0); 83 | lastNumbers[d] = num; 84 | 85 | numbers[i] = delta; 86 | } 87 | } 88 | 89 | return this.encodeFloats(numbers, options); 90 | }, 91 | 92 | decodeDeltas: function(encoded, options) { 93 | options = defaultOptions(options); 94 | 95 | var lastNumbers = []; 96 | 97 | var numbers = this.decodeFloats(encoded, options); 98 | for (var i = 0, len = numbers.length; i < len;) { 99 | for (var d = 0; d < options.dimension; ++d, ++i) { 100 | numbers[i] = Math.round((lastNumbers[d] = numbers[i] + (lastNumbers[d] || 0)) * options.factor) / options.factor; 101 | } 102 | } 103 | 104 | return numbers; 105 | }, 106 | 107 | encodeFloats: function(numbers, options) { 108 | options = defaultOptions(options); 109 | 110 | for (var i = 0, len = numbers.length; i < len; ++i) { 111 | numbers[i] = Math.round(numbers[i] * options.factor); 112 | } 113 | 114 | return this.encodeSignedIntegers(numbers); 115 | }, 116 | 117 | decodeFloats: function(encoded, options) { 118 | options = defaultOptions(options); 119 | 120 | var numbers = this.decodeSignedIntegers(encoded); 121 | for (var i = 0, len = numbers.length; i < len; ++i) { 122 | numbers[i] /= options.factor; 123 | } 124 | 125 | return numbers; 126 | }, 127 | 128 | /* jshint bitwise:false */ 129 | 130 | encodeSignedIntegers: function(numbers) { 131 | for (var i = 0, len = numbers.length; i < len; ++i) { 132 | var num = numbers[i]; 133 | numbers[i] = (num < 0) ? ~(num << 1) : (num << 1); 134 | } 135 | 136 | return this.encodeUnsignedIntegers(numbers); 137 | }, 138 | 139 | decodeSignedIntegers: function(encoded) { 140 | var numbers = this.decodeUnsignedIntegers(encoded); 141 | 142 | for (var i = 0, len = numbers.length; i < len; ++i) { 143 | var num = numbers[i]; 144 | numbers[i] = (num & 1) ? ~(num >> 1) : (num >> 1); 145 | } 146 | 147 | return numbers; 148 | }, 149 | 150 | encodeUnsignedIntegers: function(numbers) { 151 | var encoded = ''; 152 | for (var i = 0, len = numbers.length; i < len; ++i) { 153 | encoded += this.encodeUnsignedInteger(numbers[i]); 154 | } 155 | return encoded; 156 | }, 157 | 158 | decodeUnsignedIntegers: function(encoded) { 159 | var numbers = []; 160 | 161 | var current = 0; 162 | var shift = 0; 163 | 164 | for (var i = 0, len = encoded.length; i < len; ++i) { 165 | var b = encoded.charCodeAt(i) - 63; 166 | 167 | current |= (b & 0x1f) << shift; 168 | 169 | if (b < 0x20) { 170 | numbers.push(current); 171 | current = 0; 172 | shift = 0; 173 | } else { 174 | shift += 5; 175 | } 176 | } 177 | 178 | return numbers; 179 | }, 180 | 181 | encodeSignedInteger: function (num) { 182 | num = (num < 0) ? ~(num << 1) : (num << 1); 183 | return this.encodeUnsignedInteger(num); 184 | }, 185 | 186 | // This function is very similar to Google's, but I added 187 | // some stuff to deal with the double slash issue. 188 | encodeUnsignedInteger: function (num) { 189 | var value, encoded = ''; 190 | while (num >= 0x20) { 191 | value = (0x20 | (num & 0x1f)) + 63; 192 | encoded += (String.fromCharCode(value)); 193 | num >>= 5; 194 | } 195 | value = num + 63; 196 | encoded += (String.fromCharCode(value)); 197 | 198 | return encoded; 199 | } 200 | 201 | /* jshint bitwise:true */ 202 | }; 203 | 204 | // Export Node module 205 | if (typeof module === 'object' && typeof module.exports === 'object') { 206 | module.exports = PolylineUtil; 207 | } 208 | 209 | // Inject functionality into Leaflet 210 | if (typeof L === 'object') { 211 | if (!(L.Polyline.prototype.fromEncoded)) { 212 | L.Polyline.fromEncoded = function (encoded, options) { 213 | return new L.Polyline(PolylineUtil.decode(encoded), options); 214 | }; 215 | } 216 | if (!(L.Polygon.prototype.fromEncoded)) { 217 | L.Polygon.fromEncoded = function (encoded, options) { 218 | return new L.Polygon(PolylineUtil.decode(encoded), options); 219 | }; 220 | } 221 | 222 | var encodeMixin = { 223 | encodePath: function () { 224 | return PolylineUtil.encode(this.getLatLngs()); 225 | } 226 | }; 227 | 228 | if (!L.Polyline.prototype.encodePath) { 229 | L.Polyline.include(encodeMixin); 230 | } 231 | if (!L.Polygon.prototype.encodePath) { 232 | L.Polygon.include(encodeMixin); 233 | } 234 | 235 | L.PolylineUtil = PolylineUtil; 236 | } 237 | })(); 238 | -------------------------------------------------------------------------------- /src/leaflet-routeboxer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name Leaflet-RouteBoxer 3 | * @version 1.0 4 | * @copyright (c) 2015 Nearest! 5 | * @author Stephan Georg 6 | * 7 | * based on 8 | * 9 | * @name RouteBoxer 10 | * @version 1.0 11 | * @copyright (c) 2010 Google Inc. 12 | * @author Thor Mitchell 13 | * 14 | * @fileoverview The RouteBoxer class takes a path, such as the Polyline for a 15 | * route generated by a Directions request, and generates a set of LatLngBounds 16 | * objects that are guaranteed to contain every point within a given distance 17 | * of that route. These LatLngBounds objects can then be used to generate 18 | * requests to spatial search services that support bounds filtering (such as 19 | * the Google Maps Data API) in order to implement search along a route. 20 | * 21 | * RouteBoxer overlays a grid of the specified size on the route, identifies 22 | * every grid cell that the route passes through, and generates a set of bounds 23 | * that cover all of these cells, and their nearest neighbours. Consequently 24 | * the bounds returned will extend up to ~3x the specified distance from the 25 | * route in places. 26 | */ 27 | 28 | /* 29 | * Licensed under the Apache License, Version 2.0 (the "License"); 30 | * you may not use this file except in compliance with the License. 31 | * You may obtain a copy of the License at 32 | * 33 | * http://www.apache.org/licenses/LICENSE-2.0 34 | * 35 | * Unless required by applicable law or agreed to in writing, software 36 | * distributed under the License is distributed on an "AS IS" BASIS, 37 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 38 | * See the License for the specific language governing permissions and 39 | * limitations under the License. 40 | */ 41 | 42 | /** 43 | * Extending the L (leaflet object) 44 | * 45 | **/ 46 | L.RouteBoxer = L.extend({ 47 | 48 | R : 6371, // earth's mean radius in km 49 | grid_ : null, 50 | latGrid_ : [], 51 | lngGrid_ : [], 52 | boxesX_ : [], 53 | boxesY_ : [], 54 | 55 | /** 56 | * Generates boxes for a given route and distance 57 | * 58 | * @param {L.LatLng[] | L.Polyline} path The path along 59 | * which to create boxes. The path object can be either an Array of 60 | * L.LatLng objects L.Polyline object. 61 | * @param {Number} range The distance in kms around the route that the generated 62 | * boxes must cover. 63 | * @return {L.LatLngBounds[]} An array of boxes that covers the whole 64 | * path. 65 | */ 66 | box: function (path, range) { 67 | // Two dimensional array representing the cells in the grid overlaid on the path 68 | this.grid_ = null; 69 | // Array that holds the latitude coordinate of each vertical grid line 70 | this.latGrid_ = []; 71 | // Array that holds the longitude coordinate of each horizontal grid line 72 | this.lngGrid_ = []; 73 | // Array of bounds that cover the whole route formed by merging cells that 74 | // the route intersects first horizontally, and then vertically 75 | this.boxesX_ = []; 76 | // Array of bounds that cover the whole route formed by merging cells that 77 | // the route intersects first vertically, and then horizontally 78 | this.boxesY_ = []; 79 | // The array of LatLngs representing the vertices of the path 80 | var vertices = null; 81 | // If necessary convert the path into an array of LatLng objects 82 | 83 | // already an array of LatLngs 84 | if (path instanceof Array) { 85 | 86 | vertices = path; 87 | // Leaflet-RouteBoxer rewrite conditions and 88 | } else if (path instanceof L.Polyline) { 89 | vertices = path.getLatLngs(); 90 | } 91 | 92 | // Build the grid that is overlaid on the route 93 | this.buildGrid_(vertices, range); 94 | // Identify the grid cells that the route intersects 95 | this.findIntersectingCells_(vertices); 96 | // Merge adjacent intersected grid cells (and their neighbours) into two sets 97 | // of bounds, both of which cover them completely 98 | this.mergeIntersectingCells_(); 99 | // Return the set of merged bounds that has the fewest elements 100 | return (this.boxesX_.length <= this.boxesY_.length ? 101 | this.boxesX_ : 102 | this.boxesY_); 103 | }, 104 | 105 | /** 106 | * 107 | * 108 | * @param {L.LatLng[] | L.Polyline} path The path along which to create boxes 109 | */ 110 | formVertices : function(path) { 111 | var narr = []; 112 | for(var x=0;x routeBounds.getSouthWest().lat; i++) { 147 | this.latGrid_.unshift(routeBoundsCenter.rhumbDestinationPoint(180, range * i).lat); 148 | } 149 | // Starting from the center define grid lines outwards horizontally until they 150 | // extend beyond the edge of the bounding box by more than one cell 151 | this.lngGrid_.push(routeBoundsCenter.lng); 152 | // Add lines from the center out to the east 153 | this.lngGrid_.push(routeBoundsCenter.rhumbDestinationPoint(90, range).lng); 154 | for (i = 2; this.lngGrid_[i - 2] < routeBounds.getNorthEast().lng; i++) { 155 | this.lngGrid_.push(routeBoundsCenter.rhumbDestinationPoint(90, range * i).lng); 156 | } 157 | // Add lines from the center out to the west 158 | for (i = 1; this.lngGrid_[1] > routeBounds.getSouthWest().lng; i++) { 159 | this.lngGrid_.unshift(routeBoundsCenter.rhumbDestinationPoint(270, range * i).lng); 160 | } 161 | // Create a two dimensional array representing this grid 162 | this.grid_ = new Array(this.lngGrid_.length); 163 | for (i = 0; i < this.grid_.length; i++) { 164 | this.grid_[i] = new Array(this.latGrid_.length); 165 | } 166 | }, 167 | 168 | /** 169 | * Find all of the cells in the overlaid grid that the path intersects 170 | * 171 | * @param {LatLng[]} vertices The vertices of the path 172 | */ 173 | findIntersectingCells_ : function (vertices) { 174 | // Find the cell where the path begins 175 | var hintXY = this.getCellCoords_(vertices[0]); 176 | // Mark that cell and it's neighbours for inclusion in the boxes 177 | this.markCell_(hintXY); 178 | // Work through each vertex on the path identifying which grid cell it is in 179 | for (var i = 1; i < vertices.length; i++) { 180 | // Use the known cell of the previous vertex to help find the cell of this vertex 181 | var gridXY = this.getGridCoordsFromHint_(vertices[i], vertices[i - 1], hintXY); 182 | if (gridXY[0] === hintXY[0] && gridXY[1] === hintXY[1]) { 183 | // This vertex is in the same cell as the previous vertex 184 | // The cell will already have been marked for inclusion in the boxes 185 | continue; 186 | } else if ((Math.abs(hintXY[0] - gridXY[0]) === 1 && hintXY[1] === gridXY[1]) || 187 | (hintXY[0] === gridXY[0] && Math.abs(hintXY[1] - gridXY[1]) === 1)) { 188 | // This vertex is in a cell that shares an edge with the previous cell 189 | // Mark this cell and it's neighbours for inclusion in the boxes 190 | this.markCell_(gridXY); 191 | } else { 192 | // This vertex is in a cell that does not share an edge with the previous 193 | // cell. This means that the path passes through other cells between 194 | // this vertex and the previous vertex, and we must determine which cells 195 | // it passes through 196 | this.getGridIntersects_(vertices[i - 1], vertices[i], hintXY, gridXY); 197 | } 198 | // Use this cell to find and compare with the next one 199 | hintXY = gridXY; 200 | } 201 | }, 202 | 203 | /** 204 | * Find the cell a path vertex is in by brute force iteration over the grid 205 | * 206 | * @param {LatLng[]} latlng The latlng of the vertex 207 | * @return {Number[][]} The cell coordinates of this vertex in the grid 208 | */ 209 | getCellCoords_ : function (latlng) { 210 | for (var x = 0; this.lngGrid_[x] < latlng.lng; x++) {} 211 | for (var y = 0; this.latGrid_[y] < latlng.lat; y++) {} 212 | return ([x - 1, y - 1]); 213 | }, 214 | 215 | /** 216 | * Find the cell a path vertex is in based on the known location of a nearby 217 | * vertex. This saves searching the whole grid when working through vertices 218 | * on the polyline by are likely to be in close proximity to each other. 219 | * 220 | * @param {LatLng[]} latlng The latlng of the vertex to locate in the grid 221 | * @param {LatLng[]} hintlatlng The latlng of the vertex with a known location 222 | * @param {Number[]} hint The cell containing the vertex with a known location 223 | * @return {Number[]} The cell coordinates of the vertex to locate in the grid 224 | */ 225 | getGridCoordsFromHint_ : function (latlng, hintlatlng, hint) { 226 | var x, y; 227 | if (latlng.lng > hintlatlng.lng) { 228 | for (x = hint[0]; this.lngGrid_[x + 1] < latlng.lng; x++) {} 229 | } else { 230 | for (x = hint[0]; this.lngGrid_[x] > latlng.lng; x--) {} 231 | } 232 | if (latlng.lat > hintlatlng.lat) { 233 | for (y = hint[1]; this.latGrid_[y + 1] < latlng.lat; y++) {} 234 | } else { 235 | for (y = hint[1]; this.latGrid_[y] > latlng.lat; y--) {} 236 | } 237 | return ([x, y]); 238 | }, 239 | 240 | /** 241 | * Identify the grid squares that a path segment between two vertices 242 | * intersects with by: 243 | * 1. Finding the bearing between the start and end of the segment 244 | * 2. Using the delta between the lat of the start and the lat of each 245 | * latGrid boundary to find the distance to each latGrid boundary 246 | * 3. Finding the lng of the intersection of the line with each latGrid 247 | * boundary using the distance to the intersection and bearing of the line 248 | * 4. Determining the x-coord on the grid of the point of intersection 249 | * 5. Filling in all squares between the x-coord of the previous intersection 250 | * (or start) and the current one (or end) at the current y coordinate, 251 | * which is known for the grid line being intersected 252 | * 253 | * @param {LatLng} start The latlng of the vertex at the start of the segment 254 | * @param {LatLng} end The latlng of the vertex at the end of the segment 255 | * @param {Number[]} startXY The cell containing the start vertex 256 | * @param {Number[]} endXY The cell containing the vend vertex 257 | */ 258 | getGridIntersects_ : function (start, end, startXY, endXY) { 259 | var edgePoint, edgeXY, i; 260 | var brng = start.rhumbBearingTo(end); // Step 1. 261 | 262 | var hint = start; 263 | var hintXY = startXY; 264 | 265 | // Handle a line segment that travels south first 266 | if (end.lat > start.lat) { 267 | // Iterate over the east to west grid lines between the start and end cells 268 | for (i = startXY[1] + 1; i <= endXY[1]; i++) { 269 | // Find the latlng of the point where the path segment intersects with 270 | // this grid line (Step 2 & 3) 271 | edgePoint = this.getGridIntersect_(start, brng, this.latGrid_[i]); 272 | // Find the cell containing this intersect point (Step 4) 273 | edgeXY = this.getGridCoordsFromHint_(edgePoint, hint, hintXY); 274 | // Mark every cell the path has crossed between this grid and the start, 275 | // or the previous east to west grid line it crossed (Step 5) 276 | this.fillInGridSquares_(hintXY[0], edgeXY[0], i - 1); 277 | // Use the point where it crossed this grid line as the reference for the 278 | // next iteration 279 | hint = edgePoint; 280 | hintXY = edgeXY; 281 | } 282 | // Mark every cell the path has crossed between the last east to west grid 283 | // line it crossed and the end (Step 5) 284 | this.fillInGridSquares_(hintXY[0], endXY[0], i - 1); 285 | } else { 286 | // Iterate over the east to west grid lines between the start and end cells 287 | for (i = startXY[1]; i > endXY[1]; i--) { 288 | // Find the latlng of the point where the path segment intersects with 289 | // this grid line (Step 2 & 3) 290 | edgePoint = this.getGridIntersect_(start, brng, this.latGrid_[i]); 291 | // Find the cell containing this intersect point (Step 4) 292 | edgeXY = this.getGridCoordsFromHint_(edgePoint, hint, hintXY); 293 | // Mark every cell the path has crossed between this grid and the start, 294 | // or the previous east to west grid line it crossed (Step 5) 295 | this.fillInGridSquares_(hintXY[0], edgeXY[0], i); 296 | // Use the point where it crossed this grid line as the reference for the 297 | // next iteration 298 | hint = edgePoint; 299 | hintXY = edgeXY; 300 | } 301 | // Mark every cell the path has crossed between the last east to west grid 302 | // line it crossed and the end (Step 5) 303 | this.fillInGridSquares_(hintXY[0], endXY[0], i); 304 | } 305 | }, 306 | 307 | /** 308 | * Find the latlng at which a path segment intersects with a given 309 | * line of latitude 310 | * 311 | * @param {LatLng} start The vertex at the start of the path segment 312 | * @param {Number} brng The bearing of the line from start to end 313 | * @param {Number} gridLineLat The latitude of the grid line being intersected 314 | * @return {LatLng} The latlng of the point where the path segment intersects 315 | * the grid line 316 | */ 317 | getGridIntersect_ : function (start, brng, gridLineLat) { 318 | var d = this.R * ((gridLineLat.toRad() - start.lat.toRad()) / Math.cos(brng.toRad())); 319 | return start.rhumbDestinationPoint(brng, d); 320 | }, 321 | 322 | /** 323 | * Mark all cells in a given row of the grid that lie between two columns 324 | * for inclusion in the boxes 325 | * 326 | * @param {Number} startx The first column to include 327 | * @param {Number} endx The last column to include 328 | * @param {Number} y The row of the cells to include 329 | */ 330 | fillInGridSquares_ : function (startx, endx, y) { 331 | var x; 332 | if (startx < endx) { 333 | for (x = startx; x <= endx; x++) { 334 | this.markCell_([x, y]); 335 | } 336 | } else { 337 | for (x = startx; x >= endx; x--) { 338 | this.markCell_([x, y]); 339 | } 340 | } 341 | }, 342 | 343 | /** 344 | * Mark a cell and the 8 immediate neighbours for inclusion in the boxes 345 | * 346 | * @param {Number[]} square The cell to mark 347 | */ 348 | markCell_ : function (cell) { 349 | var x = cell[0]; 350 | var y = cell[1]; 351 | 352 | this.grid_[x - 1][y - 1] = 1; 353 | this.grid_[x][y - 1] = 1; 354 | this.grid_[x + 1][y - 1] = 1; 355 | this.grid_[x - 1][y] = 1; 356 | this.grid_[x][y] = 1; 357 | this.grid_[x + 1][y] = 1; 358 | this.grid_[x - 1][y + 1] = 1; 359 | this.grid_[x][y + 1] = 1; 360 | this.grid_[x + 1][y + 1] = 1; 361 | }, 362 | 363 | /** 364 | * Create two sets of bounding boxes, both of which cover all of the cells that 365 | * have been marked for inclusion. 366 | * 367 | * The first set is created by combining adjacent cells in the same column into 368 | * a set of vertical rectangular boxes, and then combining boxes of the same 369 | * height that are adjacent horizontally. 370 | * 371 | * The second set is created by combining adjacent cells in the same row into 372 | * a set of horizontal rectangular boxes, and then combining boxes of the same 373 | * width that are adjacent vertically. 374 | * 375 | */ 376 | mergeIntersectingCells_ : function () { 377 | var x, y, box; 378 | // The box we are currently expanding with new cells 379 | var currentBox = null; 380 | // Traverse the grid a row at a time 381 | for (y = 0; y < this.grid_[0].length; y++) { 382 | for (x = 0; x < this.grid_.length; x++) { 383 | if (this.grid_[x][y]) { 384 | // This cell is marked for inclusion. If the previous cell in this 385 | // row was also marked for inclusion, merge this cell into it's box. 386 | // Otherwise start a new box. 387 | box = this.getCellBounds_([x, y]); 388 | if (currentBox) { 389 | currentBox.extend(box.getNorthEast()); 390 | } else { 391 | currentBox = box; 392 | } 393 | } else { 394 | // This cell is not marked for inclusion. If the previous cell was 395 | // marked for inclusion, merge it's box with a box that spans the same 396 | // columns from the row below if possible. 397 | this.mergeBoxesY_(currentBox); 398 | currentBox = null; 399 | } 400 | } 401 | // If the last cell was marked for inclusion, merge it's box with a matching 402 | // box from the row below if possible. 403 | this.mergeBoxesY_(currentBox); 404 | currentBox = null; 405 | } 406 | // Traverse the grid a column at a time 407 | for (x = 0; x < this.grid_.length; x++) { 408 | for (y = 0; y < this.grid_[0].length; y++) { 409 | if (this.grid_[x][y]) { 410 | // This cell is marked for inclusion. If the previous cell in this 411 | // column was also marked for inclusion, merge this cell into it's box. 412 | // Otherwise start a new box. 413 | if (currentBox) { 414 | box = this.getCellBounds_([x, y]); 415 | currentBox.extend(box.getNorthEast()); 416 | } else { 417 | currentBox = this.getCellBounds_([x, y]); 418 | } 419 | } else { 420 | // This cell is not marked for inclusion. If the previous cell was 421 | // marked for inclusion, merge it's box with a box that spans the same 422 | // rows from the column to the left if possible. 423 | this.mergeBoxesX_(currentBox); 424 | currentBox = null; 425 | } 426 | } 427 | // If the last cell was marked for inclusion, merge it's box with a matching 428 | // box from the column to the left if possible. 429 | this.mergeBoxesX_(currentBox); 430 | currentBox = null; 431 | } 432 | }, 433 | 434 | /** 435 | * Search for an existing box in an adjacent row to the given box that spans the 436 | * same set of columns and if one is found merge the given box into it. If one 437 | * is not found, append this box to the list of existing boxes. 438 | * 439 | * @param {LatLngBounds} The box to merge 440 | */ 441 | mergeBoxesX_ : function (box) { 442 | if (box !== null) { 443 | for (var i = 0; i < this.boxesX_.length; i++) { 444 | if (this.boxesX_[i].getNorthEast().lng === box.getSouthWest().lng && 445 | this.boxesX_[i].getSouthWest().lat === box.getSouthWest().lat && 446 | this.boxesX_[i].getNorthEast().lat === box.getNorthEast().lat) { 447 | this.boxesX_[i].extend(box.getNorthEast()); 448 | return; 449 | } 450 | } 451 | this.boxesX_.push(box); 452 | } 453 | }, 454 | 455 | /** 456 | * Search for an existing box in an adjacent column to the given box that spans 457 | * the same set of rows and if one is found merge the given box into it. If one 458 | * is not found, append this box to the list of existing boxes. 459 | * 460 | * @param {LatLngBounds} The box to merge 461 | */ 462 | mergeBoxesY_ : function (box) { 463 | if (box !== null) { 464 | for (var i = 0; i < this.boxesY_.length; i++) { 465 | if (this.boxesY_[i].getNorthEast().lat === box.getSouthWest().lat && 466 | this.boxesY_[i].getSouthWest().lng === box.getSouthWest().lng && 467 | this.boxesY_[i].getNorthEast().lng === box.getNorthEast().lng) { 468 | this.boxesY_[i].extend(box.getNorthEast()); 469 | return; 470 | } 471 | } 472 | this.boxesY_.push(box); 473 | } 474 | }, 475 | 476 | /** 477 | * Obtain the LatLng of the origin of a cell on the grid 478 | * 479 | * @param {Number[]} cell The cell to lookup. 480 | * @return {LatLng} The latlng of the origin of the cell. 481 | */ 482 | getCellBounds_ : function (cell) { 483 | return new L.LatLngBounds( 484 | new L.LatLng(this.latGrid_[cell[1]], this.lngGrid_[cell[0]]), 485 | new L.LatLng(this.latGrid_[cell[1] + 1], this.lngGrid_[cell[0] + 1])); 486 | }, 487 | 488 | }); 489 | 490 | /* Based the on the Latitude/longitude spherical geodesy formulae & scripts 491 | at http://www.movable-type.co.uk/scripts/latlong.html 492 | (c) Chris Veness 2002-2010 493 | */ 494 | L.LatLng.prototype.rhumbDestinationPoint = function (brng, dist) { 495 | var R = 6371; // earth's mean radius in km 496 | var d = parseFloat(dist) / R; // d = angular distance covered on earth's surface 497 | var lat1 = this.lat.toRad(), lon1 = this.lng.toRad(); 498 | brng = brng.toRad(); 499 | 500 | var lat2 = lat1 + d * Math.cos(brng); 501 | var dLat = lat2 - lat1; 502 | var dPhi = Math.log(Math.tan(lat2 / 2 + Math.PI / 4) / Math.tan(lat1 / 2 + Math.PI / 4)); 503 | var q = (Math.abs(dLat) > 1e-10) ? dLat / dPhi : Math.cos(lat1); 504 | var dLon = d * Math.sin(brng) / q; 505 | // check for going past the pole 506 | if (Math.abs(lat2) > Math.PI / 2) { 507 | lat2 = lat2 > 0 ? Math.PI - lat2 : - (Math.PI - lat2); 508 | } 509 | var lon2 = (lon1 + dLon + Math.PI) % (2 * Math.PI) - Math.PI; 510 | 511 | if (isNaN(lat2) || isNaN(lon2)) { 512 | return null; 513 | } 514 | return new L.LatLng(lat2.toDeg(), lon2.toDeg()); 515 | }; 516 | 517 | L.LatLng.prototype.rhumbBearingTo = function (dest) { 518 | var dLon = (dest.lng - this.lng).toRad(); 519 | var dPhi = Math.log(Math.tan(dest.lat.toRad() / 2 + Math.PI / 4) / Math.tan(this.lat.toRad() / 2 + Math.PI / 4)); 520 | if (Math.abs(dLon) > Math.PI) { 521 | dLon = dLon > 0 ? -(2 * Math.PI - dLon) : (2 * Math.PI + dLon); 522 | } 523 | return Math.atan2(dLon, dPhi).toBrng(); 524 | }; 525 | 526 | /** 527 | * Extend the Number object to convert degrees to radians 528 | * 529 | * @return {Number} Bearing in radians 530 | * @ignore 531 | */ 532 | Number.prototype.toRad = function () { 533 | return this * Math.PI / 180; 534 | }; 535 | 536 | /** 537 | * Extend the Number object to convert radians to degrees 538 | * 539 | * @return {Number} Bearing in degrees 540 | * @ignore 541 | */ 542 | Number.prototype.toDeg = function () { 543 | return this * 180 / Math.PI; 544 | }; 545 | 546 | /** 547 | * Normalize a heading in degrees to between 0 and +360 548 | * 549 | * @return {Number} Return 550 | * @ignore 551 | */ 552 | Number.prototype.toBrng = function () { 553 | return (this.toDeg() + 360) % 360; 554 | }; 555 | --------------------------------------------------------------------------------