├── .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 |
--------------------------------------------------------------------------------