├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── fonts │ ├── OpenSans-Bold-webfont.eot │ ├── OpenSans-Bold-webfont.svg │ ├── OpenSans-Bold-webfont.woff │ ├── OpenSans-BoldItalic-webfont.eot │ ├── OpenSans-BoldItalic-webfont.svg │ ├── OpenSans-BoldItalic-webfont.woff │ ├── OpenSans-Italic-webfont.eot │ ├── OpenSans-Italic-webfont.svg │ ├── OpenSans-Italic-webfont.woff │ ├── OpenSans-Light-webfont.eot │ ├── OpenSans-Light-webfont.svg │ ├── OpenSans-Light-webfont.woff │ ├── OpenSans-LightItalic-webfont.eot │ ├── OpenSans-LightItalic-webfont.svg │ ├── OpenSans-LightItalic-webfont.woff │ ├── OpenSans-Regular-webfont.eot │ ├── OpenSans-Regular-webfont.svg │ ├── OpenSans-Regular-webfont.woff │ ├── OpenSans-Semibold-webfont.eot │ ├── OpenSans-Semibold-webfont.svg │ ├── OpenSans-Semibold-webfont.ttf │ ├── OpenSans-Semibold-webfont.woff │ ├── OpenSans-SemiboldItalic-webfont.eot │ ├── OpenSans-SemiboldItalic-webfont.svg │ ├── OpenSans-SemiboldItalic-webfont.ttf │ └── OpenSans-SemiboldItalic-webfont.woff ├── global.html ├── index.html ├── leaflet.geometryutil.js.html ├── scripts │ ├── linenumber.js │ └── prettify │ │ ├── Apache-License-2.0.txt │ │ ├── lang-css.js │ │ └── prettify.js ├── styles │ ├── jsdoc-default.css │ ├── prettify-jsdoc.css │ └── prettify-tomorrow.css ├── tutorial-closest.html └── tutorial-distance-length.html ├── jsdoc.config ├── package.json ├── spec ├── index.html ├── test.accumulatedLengths.js ├── test.angle.js ├── test.bearing.js ├── test.closest.js ├── test.closestCircle.js ├── test.closestLayer.js ├── test.closestLayerSnap.js ├── test.closestSegment.js ├── test.computeAngle.js ├── test.computeSlope.js ├── test.destination.js ├── test.distanceSegment.js ├── test.extract.js ├── test.interpolateOnLine.js ├── test.interpolateOnPointSegment.js ├── test.isAfter.js ├── test.isBefore.js ├── test.layersWithin.js ├── test.length.js ├── test.locateOnLine.js ├── test.nClosestLayers.js ├── test.readableDistance.js ├── test.reverse.js ├── test.rotatePoint.js └── test.startsAtExtremity.js ├── src ├── leaflet.geometryutil.d.ts └── leaflet.geometryutil.js └── tutorials ├── closest.html └── distance-length.html /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /package-lock.json 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "lts/*" 5 | - "v8" 6 | - "v10" 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Makina Corpus 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | * Neither the name of ODE nor the names of its contributors 14 | may be used to endorse or promote products derived from this software 15 | without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 21 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 22 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | npm 2 | npm 3 | 4 | Leaflet.GeometryUtil 5 | ==================== 6 | 7 | * Tested with stable Leaflet 0.7.0 8 | * Tested with Leaflet 1.0.0-rc.3 9 | 10 | Usage 11 | ----- 12 | 13 | Using Node: 14 | 15 | ``` 16 | npm install leaflet-geometryutil 17 | ``` 18 | 19 | Or browser: 20 | 21 | ``` 22 | 23 | ``` 24 | 25 | 26 | Check out [online documentation](http://makinacorpus.github.io/Leaflet.GeometryUtil/). 27 | 28 | 29 | Development 30 | ----------- 31 | 32 | ### Running tests in command-line 33 | 34 | * Install [nodejs](http://nodejs.org) 35 | 36 | ``` 37 | sudo apt-get install nodejs 38 | 39 | npm install 40 | ``` 41 | 42 | * Ready ! 43 | 44 | ``` 45 | npm test 46 | ``` 47 | 48 | Changelog 49 | --------- 50 | 51 | ### 0.10.3 ### 52 | 53 | * add support for closestOnCircle (#101, thanks to @danyhoron) 54 | 55 | ### 0.10.2 ### 56 | 57 | * use leaflet's earth radius in destination function (#96, thanks to @viliusstanga) 58 | * Changed imports to acommodate to new ngx-leaflet organization (#97, thanks to @rtrevinnoc) 59 | 60 | ### 0.10.1 ### 61 | 62 | * Enhance precision for meter values (metric / imperial) (#94, thanks @karlbeecken) 63 | 64 | ### 0.10.0 ### 65 | 66 | * Add TypeScript definitions (#90, thanks @cdauth) 67 | 68 | ### 0.9.3 ### 69 | 70 | * Increase locateOnLine() tolerance 71 | 72 | ### 0.9.2 ### 73 | 74 | * Fixes a crash if a multilinestring has an element with only one vertex (fixes #84, thanks @runette) 75 | 76 | ### 0.9.1 ### 77 | 78 | * Fix `locateOnLine()` doesn't return correct subline (#79, thanks @lepetittim) 79 | 80 | ### 0.9.0 ### 81 | 82 | * Fix `interpolateOnLine()` doesn't return correct predecessor (#66, thanks @jb2b38) 83 | * Add `angle()` and `destinationOnSegment()` (#71, thanks @trandaison) 84 | 85 | ### 0.8.1 ### 86 | 87 | * Remove a deprecated function in Leaflet 1.x (#69) 88 | 89 | ### 0.8.0 ### 90 | 91 | * Update leaflet dependency to `>=0.7.0` (#64, thanks @kozze89) 92 | * Add `nClosestLayer` (#62, thanks @haoliangyu) 93 | 94 | ### 0.7.2 ### 95 | 96 | * Fix #59, `closest` method using a shallow copy of latLngs => deep copy now 97 | 98 | ### 0.7.1 ### 99 | 100 | * Fix `closest` method for last segment on Polygon and nested Polygons 101 | 102 | ### 0.7.0 ### 103 | 104 | * Tested for Leaflet 1.0.0-rc.3 105 | 106 | ### 0.6.0 ### 107 | 108 | * Add nested arrays for `layer` param in `closest` method 109 | 110 | ### 0.5.1 ### 111 | 112 | * Fix closestLayer to be able to work with GeoJSON nested layers 113 | * Restrict closest method to Array and L.Polyline (L.Polygon extend L.Polyline) 114 | 115 | ### 0.5.0 ### 116 | 117 | * Add function `layersWithin()` (#34, thanks @haoliangyu) 118 | * Fix safety check on the ratio value in ``interpolateOnLine()` (#29, thanks @Marcussacapuces91) 119 | 120 | ### 0.4.0 ### 121 | 122 | * Same version as v0.3.3, new release as v0.4.0 to keep numbering coherent as a new feature has been added 123 | 124 | ### 0.3.3 ### 125 | 126 | * Add bearing and destination functions (thanks @doublestranded) 127 | 128 | ### 0.3.2 ### 129 | 130 | * Use a soft dependency for Leaflet (thanks Erik Escoffier) 131 | 132 | ### 0.3.1 ### 133 | 134 | * Make sure interpolateOnLine() always returns a L.LatLng object (thanks Justin Manley) 135 | 136 | ### 0.3.0 ### 137 | 138 | * Added UMD style initialization (thanks @PerLiedman) 139 | * Added readable distance (thanks @Mylen) 140 | * Fix side effects on latlngs with `closest()` (thanks @AndrewIngram) 141 | 142 | ### 0.2.0 ### 143 | 144 | * Locate point on line 145 | * Rotate point around center 146 | * Fixed bug if closest point was on last segment 147 | 148 | ### 0.1.0 ### 149 | 150 | * Line subpart extraction 151 | * Line lengths 152 | * Angle and slope computation 153 | * Line reverse 154 | * Line interpolation 155 | 156 | ### 0.0.1 ### 157 | 158 | * Initial working version 159 | 160 | 161 | License 162 | ------- 163 | 164 | * BSD New 165 | 166 | 167 | Authors 168 | ------- 169 | 170 | * [Benjamin Becquet](https://github.com/bbecquet) 171 | * [Mathieu Leplatre](https://github.com/leplatrem) 172 | * [Simon Thépot](https://github.com/djcoin) 173 | * [Nhinze](https://github.com/nhinze) 174 | * [Frédéric Bonifas](https://github.com/fredericbonifas) 175 | * [Alexander Melard](https://github.com/mylen) 176 | 177 | [![Makina Corpus](http://depot.makina-corpus.org/public/logo.gif)](http://makinacorpus.com) 178 | -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-Bold-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-Bold-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-BoldItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-BoldItalic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-Italic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-Italic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-Light-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-Light-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-LightItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-LightItalic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-Regular-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-Regular-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Semibold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-Semibold-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Semibold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-Semibold-webfont.ttf -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Semibold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-Semibold-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-SemiboldItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-SemiboldItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-SemiboldItalic-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-SemiboldItalic-webfont.ttf -------------------------------------------------------------------------------- /docs/fonts/OpenSans-SemiboldItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/Leaflet.GeometryUtil/75fc60255cc973c931c069f281b6514a8904ee21/docs/fonts/OpenSans-SemiboldItalic-webfont.woff -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Home - Documentation 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 29 | 30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 |

Leaflet.GeometryUtil

52 |

Build Status

53 |
    54 |
  • Tested with stable Leaflet 0.7.0
  • 55 |
  • Tested with Leaflet 1.0.0-rc.3
  • 56 |
57 |

Usage

58 |

Using Node:

59 |
    npm install leaflet-geometryutil
 60 | 
61 |

Or browser:

62 |
    <script src="leaflet.geometryutil.js"></script>
 63 | 
64 |

Check out online documentation.

65 |

Development

66 |

Running tests in command-line

67 | 70 |
    sudo apt-get install nodejs
 71 | 
 72 |     npm install
 73 | 
74 |
    75 |
  • Ready !
  • 76 |
77 |
    npm test
 78 | 
79 |

Changelog

80 |

0.9.3

81 |
    82 |
  • Increase locateOnLine() tolerance
  • 83 |
84 |

0.9.2

85 |
    86 |
  • Fixes a crash if a multilinestring has an element with only one vertex (fixes #84, thanks @runette)
  • 87 |
88 |

0.9.1

89 |
    90 |
  • Fix locateOnLine() doesn't return correct subline (#79, thanks @lepetittim)
  • 91 |
92 |

0.9.0

93 |
    94 |
  • Fix interpolateOnLine() doesn't return correct predecessor (#66, thanks @jb2b38)
  • 95 |
  • Add angle() and destinationOnSegment() (#71, thanks @trandaison)
  • 96 |
97 |

0.8.1

98 |
    99 |
  • Remove a deprecated function in Leaflet 1.x (#69)
  • 100 |
101 |

0.8.0

102 |
    103 |
  • Update leaflet dependency to >=0.7.0 (#64, thanks @kozze89)
  • 104 |
  • Add nClosestLayer (#62, thanks @haoliangyu)
  • 105 |
106 |

0.7.2

107 |
    108 |
  • Fix #59, closest method using a shallow copy of latLngs => deep copy now
  • 109 |
110 |

0.7.1

111 |
    112 |
  • Fix closest method for last segment on Polygon and nested Polygons
  • 113 |
114 |

0.7.0

115 |
    116 |
  • Tested for Leaflet 1.0.0-rc.3
  • 117 |
118 |

0.6.0

119 |
    120 |
  • Add nested arrays for layer param in closest method
  • 121 |
122 |

0.5.1

123 |
    124 |
  • Fix closestLayer to be able to work with GeoJSON nested layers
  • 125 |
  • Restrict closest method to Array and L.Polyline (L.Polygon extend L.Polyline)
  • 126 |
127 |

0.5.0

128 |
    129 |
  • Add function layersWithin() (#34, thanks @haoliangyu)
  • 130 |
  • Fix safety check on the ratio value in ``interpolateOnLine()` (#29, thanks @Marcussacapuces91)
  • 131 |
132 |

0.4.0

133 |
    134 |
  • Same version as v0.3.3, new release as v0.4.0 to keep numbering coherent as a new feature has been added
  • 135 |
136 |

0.3.3

137 |
    138 |
  • Add bearing and destination functions (thanks @doublestranded)
  • 139 |
140 |

0.3.2

141 |
    142 |
  • Use a soft dependency for Leaflet (thanks Erik Escoffier)
  • 143 |
144 |

0.3.1

145 |
    146 |
  • Make sure interpolateOnLine() always returns a L.LatLng object (thanks Justin Manley)
  • 147 |
148 |

0.3.0

149 |
    150 |
  • Added UMD style initialization (thanks @PerLiedman)
  • 151 |
  • Added readable distance (thanks @Mylen)
  • 152 |
  • Fix side effects on latlngs with closest() (thanks @AndrewIngram)
  • 153 |
154 |

0.2.0

155 |
    156 |
  • Locate point on line
  • 157 |
  • Rotate point around center
  • 158 |
  • Fixed bug if closest point was on last segment
  • 159 |
160 |

0.1.0

161 |
    162 |
  • Line subpart extraction
  • 163 |
  • Line lengths
  • 164 |
  • Angle and slope computation
  • 165 |
  • Line reverse
  • 166 |
  • Line interpolation
  • 167 |
168 |

0.0.1

169 |
    170 |
  • Initial working version
  • 171 |
172 |

License

173 |
    174 |
  • BSD New
  • 175 |
176 |

Authors

177 | 185 |

Makina Corpus

186 |
187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 |
197 | 198 |
199 | 200 |

201 | L.GeometryUtil 202 |

203 | 204 | 205 |
206 | 207 |
208 |
209 | 210 | 211 |

Leaflet Geometry utilities for distances and linear referencing.

212 | 213 | 214 | 215 | 216 | 217 |
218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 |
Source:
245 |
248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 |
256 | 257 | 258 | 259 | 260 |
261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 |
280 | 281 |
282 | 283 | 284 | 285 | 286 |
287 | 288 |
289 | 290 | 293 | 294 | 295 | 296 | 297 | -------------------------------------------------------------------------------- /docs/leaflet.geometryutil.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | leaflet.geometryutil.js - Documentation 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 29 | 30 |
31 | 32 |

leaflet.geometryutil.js

33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 |
42 |
// Packaging/modules magic dance.
 43 | (function (factory) {
 44 |     var L;
 45 |     if (typeof define === 'function' && define.amd) {
 46 |         // AMD
 47 |         define(['leaflet'], factory);
 48 |     } else if (typeof module !== 'undefined') {
 49 |         // Node/CommonJS
 50 |         L = require('leaflet');
 51 |         module.exports = factory(L);
 52 |     } else {
 53 |         // Browser globals
 54 |         if (typeof window.L === 'undefined')
 55 |             throw 'Leaflet must be loaded first';
 56 |         factory(window.L);
 57 |     }
 58 | }(function (L) {
 59 | "use strict";
 60 | 
 61 | L.Polyline._flat = L.LineUtil.isFlat || L.Polyline._flat || function (latlngs) {
 62 |     // true if it's a flat array of latlngs; false if nested
 63 |     return !L.Util.isArray(latlngs[0]) || (typeof latlngs[0][0] !== 'object' && typeof latlngs[0][0] !== 'undefined');
 64 | };
 65 | 
 66 | /**
 67 |  * @fileOverview Leaflet Geometry utilities for distances and linear referencing.
 68 |  * @name L.GeometryUtil
 69 |  */
 70 | 
 71 | L.GeometryUtil = L.extend(L.GeometryUtil || {}, {
 72 | 
 73 |     /**
 74 |         Shortcut function for planar distance between two {L.LatLng} at current zoom.
 75 | 
 76 |         @tutorial distance-length
 77 | 
 78 |         @param {L.Map} map Leaflet map to be used for this method
 79 |         @param {L.LatLng} latlngA geographical point A
 80 |         @param {L.LatLng} latlngB geographical point B
 81 |         @returns {Number} planar distance
 82 |      */
 83 |     distance: function (map, latlngA, latlngB) {
 84 |         return map.latLngToLayerPoint(latlngA).distanceTo(map.latLngToLayerPoint(latlngB));
 85 |     },
 86 | 
 87 |     /**
 88 |         Shortcut function for planar distance between a {L.LatLng} and a segment (A-B).
 89 |         @param {L.Map} map Leaflet map to be used for this method
 90 |         @param {L.LatLng} latlng - The position to search
 91 |         @param {L.LatLng} latlngA geographical point A of the segment
 92 |         @param {L.LatLng} latlngB geographical point B of the segment
 93 |         @returns {Number} planar distance
 94 |     */
 95 |     distanceSegment: function (map, latlng, latlngA, latlngB) {
 96 |         var p = map.latLngToLayerPoint(latlng),
 97 |            p1 = map.latLngToLayerPoint(latlngA),
 98 |            p2 = map.latLngToLayerPoint(latlngB);
 99 |         return L.LineUtil.pointToSegmentDistance(p, p1, p2);
100 |     },
101 | 
102 |     /**
103 |         Shortcut function for converting distance to readable distance.
104 |         @param {Number} distance distance to be converted
105 |         @param {String} unit 'metric' or 'imperial'
106 |         @returns {String} in yard or miles
107 |     */
108 |     readableDistance: function (distance, unit) {
109 |         var isMetric = (unit !== 'imperial'),
110 |             distanceStr;
111 |         if (isMetric) {
112 |             // show metres when distance is < 1km, then show km
113 |             if (distance > 1000) {
114 |                 distanceStr = (distance  / 1000).toFixed(2) + ' km';
115 |             }
116 |             else {
117 |                 distanceStr = Math.ceil(distance) + ' m';
118 |             }
119 |         }
120 |         else {
121 |             distance *= 1.09361;
122 |             if (distance > 1760) {
123 |                 distanceStr = (distance / 1760).toFixed(2) + ' miles';
124 |             }
125 |             else {
126 |                 distanceStr = Math.ceil(distance) + ' yd';
127 |             }
128 |         }
129 |         return distanceStr;
130 |     },
131 | 
132 |     /**
133 |         Returns true if the latlng belongs to segment A-B
134 |         @param {L.LatLng} latlng - The position to search
135 |         @param {L.LatLng} latlngA geographical point A of the segment
136 |         @param {L.LatLng} latlngB geographical point B of the segment
137 |         @param {?Number} [tolerance=0.2] tolerance to accept if latlng belongs really
138 |         @returns {boolean}
139 |      */
140 |     belongsSegment: function(latlng, latlngA, latlngB, tolerance) {
141 |         tolerance = tolerance === undefined ? 0.2 : tolerance;
142 |         var hypotenuse = latlngA.distanceTo(latlngB),
143 |             delta = latlngA.distanceTo(latlng) + latlng.distanceTo(latlngB) - hypotenuse;
144 |         return delta/hypotenuse < tolerance;
145 |     },
146 | 
147 |     /**
148 |      * Returns total length of line
149 |      * @tutorial distance-length
150 |      *
151 |      * @param {L.Polyline|Array<L.Point>|Array<L.LatLng>} coords Set of coordinates
152 |      * @returns {Number} Total length (pixels for Point, meters for LatLng)
153 |      */
154 |     length: function (coords) {
155 |         var accumulated = L.GeometryUtil.accumulatedLengths(coords);
156 |         return accumulated.length > 0 ? accumulated[accumulated.length-1] : 0;
157 |     },
158 | 
159 |     /**
160 |      * Returns a list of accumulated length along a line.
161 |      * @param {L.Polyline|Array<L.Point>|Array<L.LatLng>} coords Set of coordinates
162 |      * @returns {Array<Number>} Array of accumulated lengths (pixels for Point, meters for LatLng)
163 |      */
164 |     accumulatedLengths: function (coords) {
165 |         if (typeof coords.getLatLngs == 'function') {
166 |             coords = coords.getLatLngs();
167 |         }
168 |         if (coords.length === 0)
169 |             return [];
170 |         var total = 0,
171 |             lengths = [0];
172 |         for (var i = 0, n = coords.length - 1; i< n; i++) {
173 |             total += coords[i].distanceTo(coords[i+1]);
174 |             lengths.push(total);
175 |         }
176 |         return lengths;
177 |     },
178 | 
179 |     /**
180 |         Returns the closest point of a {L.LatLng} on the segment (A-B)
181 | 
182 |         @tutorial closest
183 | 
184 |         @param {L.Map} map Leaflet map to be used for this method
185 |         @param {L.LatLng} latlng - The position to search
186 |         @param {L.LatLng} latlngA geographical point A of the segment
187 |         @param {L.LatLng} latlngB geographical point B of the segment
188 |         @returns {L.LatLng} Closest geographical point
189 |     */
190 |     closestOnSegment: function (map, latlng, latlngA, latlngB) {
191 |         var maxzoom = map.getMaxZoom();
192 |         if (maxzoom === Infinity)
193 |             maxzoom = map.getZoom();
194 |         var p = map.project(latlng, maxzoom),
195 |            p1 = map.project(latlngA, maxzoom),
196 |            p2 = map.project(latlngB, maxzoom),
197 |            closest = L.LineUtil.closestPointOnSegment(p, p1, p2);
198 |         return map.unproject(closest, maxzoom);
199 |     },
200 | 
201 |     /**
202 |         Returns the closest latlng on layer.
203 | 
204 |         Accept nested arrays
205 | 
206 |         @tutorial closest
207 | 
208 |         @param {L.Map} map Leaflet map to be used for this method
209 |         @param {Array<L.LatLng>|Array<Array<L.LatLng>>|L.PolyLine|L.Polygon} layer - Layer that contains the result
210 |         @param {L.LatLng} latlng - The position to search
211 |         @param {?boolean} [vertices=false] - Whether to restrict to path vertices.
212 |         @returns {L.LatLng} Closest geographical point or null if layer param is incorrect
213 |     */
214 |     closest: function (map, layer, latlng, vertices) {
215 | 
216 |         var latlngs,
217 |             mindist = Infinity,
218 |             result = null,
219 |             i, n, distance, subResult;
220 | 
221 |         if (layer instanceof Array) {
222 |             // if layer is Array<Array<T>>
223 |             if (layer[0] instanceof Array && typeof layer[0][0] !== 'number') {
224 |                 // if we have nested arrays, we calc the closest for each array
225 |                 // recursive
226 |                 for (i = 0; i < layer.length; i++) {
227 |                     subResult = L.GeometryUtil.closest(map, layer[i], latlng, vertices);
228 |                     if (subResult && subResult.distance < mindist) {
229 |                         mindist = subResult.distance;
230 |                         result = subResult;
231 |                     }
232 |                 }
233 |                 return result;
234 |             } else if (layer[0] instanceof L.LatLng
235 |                         || typeof layer[0][0] === 'number'
236 |                         || typeof layer[0].lat === 'number') { // we could have a latlng as [x,y] with x & y numbers or {lat, lng}
237 |                 layer = L.polyline(layer);
238 |             } else {
239 |                 return result;
240 |             }
241 |         }
242 | 
243 |         // if we don't have here a Polyline, that means layer is incorrect
244 |         // see https://github.com/makinacorpus/Leaflet.GeometryUtil/issues/23
245 |         if (! ( layer instanceof L.Polyline ) )
246 |             return result;
247 | 
248 |         // deep copy of latlngs
249 |         latlngs = JSON.parse(JSON.stringify(layer.getLatLngs().slice(0)));
250 | 
251 |         // add the last segment for L.Polygon
252 |         if (layer instanceof L.Polygon) {
253 |             // add the last segment for each child that is a nested array
254 |             var addLastSegment = function(latlngs) {
255 |                 if (L.Polyline._flat(latlngs)) {
256 |                     latlngs.push(latlngs[0]);
257 |                 } else {
258 |                     for (var i = 0; i < latlngs.length; i++) {
259 |                         addLastSegment(latlngs[i]);
260 |                     }
261 |                 }
262 |             };
263 |             addLastSegment(latlngs);
264 |         }
265 | 
266 |         // we have a multi polygon / multi polyline / polygon with holes
267 |         // use recursive to explore and return the good result
268 |         if ( ! L.Polyline._flat(latlngs) ) {
269 |             for (i = 0; i < latlngs.length; i++) {
270 |                 // if we are at the lower level, and if we have a L.Polygon, we add the last segment
271 |                 subResult = L.GeometryUtil.closest(map, latlngs[i], latlng, vertices);
272 |                 if (subResult.distance < mindist) {
273 |                     mindist = subResult.distance;
274 |                     result = subResult;
275 |                 }
276 |             }
277 |             return result;
278 | 
279 |         } else {
280 | 
281 |             // Lookup vertices
282 |             if (vertices) {
283 |                 for(i = 0, n = latlngs.length; i < n; i++) {
284 |                     var ll = latlngs[i];
285 |                     distance = L.GeometryUtil.distance(map, latlng, ll);
286 |                     if (distance < mindist) {
287 |                         mindist = distance;
288 |                         result = ll;
289 |                         result.distance = distance;
290 |                     }
291 |                 }
292 |                 return result;
293 |             }
294 | 
295 |             // Keep the closest point of all segments
296 |             for (i = 0, n = latlngs.length; i < n-1; i++) {
297 |                 var latlngA = latlngs[i],
298 |                     latlngB = latlngs[i+1];
299 |                 distance = L.GeometryUtil.distanceSegment(map, latlng, latlngA, latlngB);
300 |                 if (distance <= mindist) {
301 |                     mindist = distance;
302 |                     result = L.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB);
303 |                     result.distance = distance;
304 |                 }
305 |             }
306 |             return result;
307 |         }
308 | 
309 |     },
310 | 
311 |     /**
312 |         Returns the closest layer to latlng among a list of layers.
313 | 
314 |         @tutorial closest
315 | 
316 |         @param {L.Map} map Leaflet map to be used for this method
317 |         @param {Array<L.ILayer>} layers Set of layers
318 |         @param {L.LatLng} latlng - The position to search
319 |         @returns {object} ``{layer, latlng, distance}`` or ``null`` if list is empty;
320 |     */
321 |     closestLayer: function (map, layers, latlng) {
322 |         var mindist = Infinity,
323 |             result = null,
324 |             ll = null,
325 |             distance = Infinity;
326 | 
327 |         for (var i = 0, n = layers.length; i < n; i++) {
328 |             var layer = layers[i];
329 |             if (layer instanceof L.LayerGroup) {
330 |                 // recursive
331 |                 var subResult = L.GeometryUtil.closestLayer(map, layer.getLayers(), latlng);
332 |                 if (subResult.distance < mindist) {
333 |                     mindist = subResult.distance;
334 |                     result = subResult;
335 |                 }
336 |             } else {
337 |                 // Single dimension, snap on points, else snap on closest
338 |                 if (typeof layer.getLatLng == 'function') {
339 |                     ll = layer.getLatLng();
340 |                     distance = L.GeometryUtil.distance(map, latlng, ll);
341 |                 }
342 |                 else {
343 |                     ll = L.GeometryUtil.closest(map, layer, latlng);
344 |                     if (ll) distance = ll.distance;  // Can return null if layer has no points.
345 |                 }
346 |                 if (distance < mindist) {
347 |                     mindist = distance;
348 |                     result = {layer: layer, latlng: ll, distance: distance};
349 |                 }
350 |             }
351 |         }
352 |         return result;
353 |     },
354 | 
355 |     /**
356 |         Returns the n closest layers to latlng among a list of input layers.
357 | 
358 |         @param {L.Map} map - Leaflet map to be used for this method
359 |         @param {Array<L.ILayer>} layers - Set of layers
360 |         @param {L.LatLng} latlng - The position to search
361 |         @param {?Number} [n=layers.length] - the expected number of output layers.
362 |         @returns {Array<object>} an array of objects ``{layer, latlng, distance}`` or ``null`` if the input is invalid (empty list or negative n)
363 |     */
364 |     nClosestLayers: function (map, layers, latlng, n) {
365 |         n = typeof n === 'number' ? n : layers.length;
366 | 
367 |         if (n < 1 || layers.length < 1) {
368 |             return null;
369 |         }
370 | 
371 |         var results = [];
372 |         var distance, ll;
373 | 
374 |         for (var i = 0, m = layers.length; i < m; i++) {
375 |             var layer = layers[i];
376 |             if (layer instanceof L.LayerGroup) {
377 |                 // recursive
378 |                 var subResult = L.GeometryUtil.closestLayer(map, layer.getLayers(), latlng);
379 |                 results.push(subResult);
380 |             } else {
381 |                 // Single dimension, snap on points, else snap on closest
382 |                 if (typeof layer.getLatLng == 'function') {
383 |                     ll = layer.getLatLng();
384 |                     distance = L.GeometryUtil.distance(map, latlng, ll);
385 |                 }
386 |                 else {
387 |                     ll = L.GeometryUtil.closest(map, layer, latlng);
388 |                     if (ll) distance = ll.distance;  // Can return null if layer has no points.
389 |                 }
390 |                 results.push({layer: layer, latlng: ll, distance: distance});
391 |             }
392 |         }
393 | 
394 |         results.sort(function(a, b) {
395 |             return a.distance - b.distance;
396 |         });
397 | 
398 |         if (results.length > n) {
399 |             return results.slice(0, n);
400 |         } else  {
401 |             return results;
402 |         }
403 |     },
404 | 
405 |     /**
406 |      * Returns all layers within a radius of the given position, in an ascending order of distance.
407 |        @param {L.Map} map Leaflet map to be used for this method
408 |        @param {Array<ILayer>} layers - A list of layers.
409 |        @param {L.LatLng} latlng - The position to search
410 |        @param {?Number} [radius=Infinity] - Search radius in pixels
411 |        @return {object[]} an array of objects including layer within the radius, closest latlng, and distance
412 |      */
413 |     layersWithin: function(map, layers, latlng, radius) {
414 |       radius = typeof radius == 'number' ? radius : Infinity;
415 | 
416 |       var results = [];
417 |       var ll = null;
418 |       var distance = 0;
419 | 
420 |       for (var i = 0, n = layers.length; i < n; i++) {
421 |         var layer = layers[i];
422 | 
423 |         if (typeof layer.getLatLng == 'function') {
424 |             ll = layer.getLatLng();
425 |             distance = L.GeometryUtil.distance(map, latlng, ll);
426 |         }
427 |         else {
428 |             ll = L.GeometryUtil.closest(map, layer, latlng);
429 |             if (ll) distance = ll.distance;  // Can return null if layer has no points.
430 |         }
431 | 
432 |         if (ll && distance < radius) {
433 |             results.push({layer: layer, latlng: ll, distance: distance});
434 |         }
435 |       }
436 | 
437 |       var sortedResults = results.sort(function(a, b) {
438 |           return a.distance - b.distance;
439 |       });
440 | 
441 |       return sortedResults;
442 |     },
443 | 
444 |     /**
445 |         Returns the closest position from specified {LatLng} among specified layers,
446 |         with a maximum tolerance in pixels, providing snapping behaviour.
447 | 
448 |         @tutorial closest
449 | 
450 |         @param {L.Map} map Leaflet map to be used for this method
451 |         @param {Array<ILayer>} layers - A list of layers to snap on.
452 |         @param {L.LatLng} latlng - The position to snap
453 |         @param {?Number} [tolerance=Infinity] - Maximum number of pixels.
454 |         @param {?boolean} [withVertices=true] - Snap to layers vertices or segment points (not only vertex)
455 |         @returns {object} with snapped {LatLng} and snapped {Layer} or null if tolerance exceeded.
456 |     */
457 |     closestLayerSnap: function (map, layers, latlng, tolerance, withVertices) {
458 |         tolerance = typeof tolerance == 'number' ? tolerance : Infinity;
459 |         withVertices = typeof withVertices == 'boolean' ? withVertices : true;
460 | 
461 |         var result = L.GeometryUtil.closestLayer(map, layers, latlng);
462 |         if (!result || result.distance > tolerance)
463 |             return null;
464 | 
465 |         // If snapped layer is linear, try to snap on vertices (extremities and middle points)
466 |         if (withVertices && typeof result.layer.getLatLngs == 'function') {
467 |             var closest = L.GeometryUtil.closest(map, result.layer, result.latlng, true);
468 |             if (closest.distance < tolerance) {
469 |                 result.latlng = closest;
470 |                 result.distance = L.GeometryUtil.distance(map, closest, latlng);
471 |             }
472 |         }
473 |         return result;
474 |     },
475 | 
476 |     /**
477 |         Returns the Point located on a segment at the specified ratio of the segment length.
478 |         @param {L.Point} pA coordinates of point A
479 |         @param {L.Point} pB coordinates of point B
480 |         @param {Number} the length ratio, expressed as a decimal between 0 and 1, inclusive.
481 |         @returns {L.Point} the interpolated point.
482 |     */
483 |     interpolateOnPointSegment: function (pA, pB, ratio) {
484 |         return L.point(
485 |             (pA.x * (1 - ratio)) + (ratio * pB.x),
486 |             (pA.y * (1 - ratio)) + (ratio * pB.y)
487 |         );
488 |     },
489 | 
490 |     /**
491 |         Returns the coordinate of the point located on a line at the specified ratio of the line length.
492 |         @param {L.Map} map Leaflet map to be used for this method
493 |         @param {Array<L.LatLng>|L.PolyLine} latlngs Set of geographical points
494 |         @param {Number} ratio the length ratio, expressed as a decimal between 0 and 1, inclusive
495 |         @returns {Object} an object with latLng ({LatLng}) and predecessor ({Number}), the index of the preceding vertex in the Polyline
496 |         (-1 if the interpolated point is the first vertex)
497 |     */
498 |     interpolateOnLine: function (map, latLngs, ratio) {
499 |         latLngs = (latLngs instanceof L.Polyline) ? latLngs.getLatLngs() : latLngs;
500 |         var n = latLngs.length;
501 |         if (n < 2) {
502 |             return null;
503 |         }
504 | 
505 |         // ensure the ratio is between 0 and 1;
506 |         ratio = Math.max(Math.min(ratio, 1), 0);
507 | 
508 |         if (ratio === 0) {
509 |             return {
510 |                 latLng: latLngs[0] instanceof L.LatLng ? latLngs[0] : L.latLng(latLngs[0]),
511 |                 predecessor: -1
512 |             };
513 |         }
514 |         if (ratio == 1) {
515 |             return {
516 |                 latLng: latLngs[latLngs.length -1] instanceof L.LatLng ? latLngs[latLngs.length -1] : L.latLng(latLngs[latLngs.length -1]),
517 |                 predecessor: latLngs.length - 2
518 |             };
519 |         }
520 | 
521 |         // project the LatLngs as Points,
522 |         // and compute total planar length of the line at max precision
523 |         var maxzoom = map.getMaxZoom();
524 |         if (maxzoom === Infinity)
525 |             maxzoom = map.getZoom();
526 |         var pts = [];
527 |         var lineLength = 0;
528 |         for(var i = 0; i < n; i++) {
529 |             pts[i] = map.project(latLngs[i], maxzoom);
530 |             if(i > 0)
531 |               lineLength += pts[i-1].distanceTo(pts[i]);
532 |         }
533 | 
534 |         var ratioDist = lineLength * ratio;
535 | 
536 | 		// follow the line segments [ab], adding lengths,
537 |         // until we find the segment where the points should lie on
538 | 		var cumulativeDistanceToA = 0, cumulativeDistanceToB = 0;
539 | 		for (var i = 0; cumulativeDistanceToB < ratioDist; i++) {
540 | 			var pointA = pts[i], pointB = pts[i+1];
541 | 
542 | 			cumulativeDistanceToA = cumulativeDistanceToB;
543 | 			cumulativeDistanceToB += pointA.distanceTo(pointB);
544 | 		}
545 | 		
546 | 		if (pointA == undefined && pointB == undefined) { // Happens when line has no length
547 | 			var pointA = pts[0], pointB = pts[1], i = 1;
548 | 		}
549 | 
550 | 		// compute the ratio relative to the segment [ab]
551 | 		var segmentRatio = ((cumulativeDistanceToB - cumulativeDistanceToA) !== 0) ? ((ratioDist - cumulativeDistanceToA) / (cumulativeDistanceToB - cumulativeDistanceToA)) : 0;
552 | 		var interpolatedPoint = L.GeometryUtil.interpolateOnPointSegment(pointA, pointB, segmentRatio);
553 | 		return {
554 | 			latLng: map.unproject(interpolatedPoint, maxzoom),
555 | 			predecessor: i-1
556 | 		};
557 |     },
558 | 
559 |     /**
560 |         Returns a float between 0 and 1 representing the location of the
561 |         closest point on polyline to the given latlng, as a fraction of total line length.
562 |         (opposite of L.GeometryUtil.interpolateOnLine())
563 |         @param {L.Map} map Leaflet map to be used for this method
564 |         @param {L.PolyLine} polyline Polyline on which the latlng will be search
565 |         @param {L.LatLng} latlng The position to search
566 |         @returns {Number} Float between 0 and 1
567 |     */
568 |     locateOnLine: function (map, polyline, latlng) {
569 |         var latlngs = polyline.getLatLngs();
570 |         if (latlng.equals(latlngs[0]))
571 |             return 0.0;
572 |         if (latlng.equals(latlngs[latlngs.length-1]))
573 |             return 1.0;
574 | 
575 |         var point = L.GeometryUtil.closest(map, polyline, latlng, false),
576 |             lengths = L.GeometryUtil.accumulatedLengths(latlngs),
577 |             total_length = lengths[lengths.length-1],
578 |             portion = 0,
579 |             found = false;
580 |         for (var i=0, n = latlngs.length-1; i < n; i++) {
581 |             var l1 = latlngs[i],
582 |                 l2 = latlngs[i+1];
583 |             portion = lengths[i];
584 |             if (L.GeometryUtil.belongsSegment(point, l1, l2, 0.001)) {
585 |                 portion += l1.distanceTo(point);
586 |                 found = true;
587 |                 break;
588 |             }
589 |         }
590 |         if (!found) {
591 |             throw "Could not interpolate " + latlng.toString() + " within " + polyline.toString();
592 |         }
593 |         return portion / total_length;
594 |     },
595 | 
596 |     /**
597 |         Returns a clone with reversed coordinates.
598 |         @param {L.PolyLine} polyline polyline to reverse
599 |         @returns {L.PolyLine} polyline reversed
600 |     */
601 |     reverse: function (polyline) {
602 |         return L.polyline(polyline.getLatLngs().slice(0).reverse());
603 |     },
604 | 
605 |     /**
606 |         Returns a sub-part of the polyline, from start to end.
607 |         If start is superior to end, returns extraction from inverted line.
608 |         @param {L.Map} map Leaflet map to be used for this method
609 |         @param {L.PolyLine} polyline Polyline on which will be extracted the sub-part
610 |         @param {Number} start ratio, expressed as a decimal between 0 and 1, inclusive
611 |         @param {Number} end ratio, expressed as a decimal between 0 and 1, inclusive
612 |         @returns {Array<L.LatLng>} new polyline
613 |      */
614 |     extract: function (map, polyline, start, end) {
615 |         if (start > end) {
616 |             return L.GeometryUtil.extract(map, L.GeometryUtil.reverse(polyline), 1.0-start, 1.0-end);
617 |         }
618 | 
619 |         // Bound start and end to [0-1]
620 |         start = Math.max(Math.min(start, 1), 0);
621 |         end = Math.max(Math.min(end, 1), 0);
622 | 
623 |         var latlngs = polyline.getLatLngs(),
624 |             startpoint = L.GeometryUtil.interpolateOnLine(map, polyline, start),
625 |             endpoint = L.GeometryUtil.interpolateOnLine(map, polyline, end);
626 |         // Return single point if start == end
627 |         if (start == end) {
628 |             var point = L.GeometryUtil.interpolateOnLine(map, polyline, end);
629 |             return [point.latLng];
630 |         }
631 |         // Array.slice() works indexes at 0
632 |         if (startpoint.predecessor == -1)
633 |             startpoint.predecessor = 0;
634 |         if (endpoint.predecessor == -1)
635 |             endpoint.predecessor = 0;
636 |         var result = latlngs.slice(startpoint.predecessor+1, endpoint.predecessor+1);
637 |         result.unshift(startpoint.latLng);
638 |         result.push(endpoint.latLng);
639 |         return result;
640 |     },
641 | 
642 |     /**
643 |         Returns true if first polyline ends where other second starts.
644 |         @param {L.PolyLine} polyline First polyline
645 |         @param {L.PolyLine} other Second polyline
646 |         @returns {bool}
647 |     */
648 |     isBefore: function (polyline, other) {
649 |         if (!other) return false;
650 |         var lla = polyline.getLatLngs(),
651 |             llb = other.getLatLngs();
652 |         return (lla[lla.length-1]).equals(llb[0]);
653 |     },
654 | 
655 |     /**
656 |         Returns true if first polyline starts where second ends.
657 |         @param {L.PolyLine} polyline First polyline
658 |         @param {L.PolyLine} other Second polyline
659 |         @returns {bool}
660 |     */
661 |     isAfter: function (polyline, other) {
662 |         if (!other) return false;
663 |         var lla = polyline.getLatLngs(),
664 |             llb = other.getLatLngs();
665 |         return (lla[0]).equals(llb[llb.length-1]);
666 |     },
667 | 
668 |     /**
669 |         Returns true if first polyline starts where second ends or start.
670 |         @param {L.PolyLine} polyline First polyline
671 |         @param {L.PolyLine} other Second polyline
672 |         @returns {bool}
673 |     */
674 |     startsAtExtremity: function (polyline, other) {
675 |         if (!other) return false;
676 |         var lla = polyline.getLatLngs(),
677 |             llb = other.getLatLngs(),
678 |             start = lla[0];
679 |         return start.equals(llb[0]) || start.equals(llb[llb.length-1]);
680 |     },
681 | 
682 |     /**
683 |         Returns horizontal angle in degres between two points.
684 |         @param {L.Point} a Coordinates of point A
685 |         @param {L.Point} b Coordinates of point B
686 |         @returns {Number} horizontal angle
687 |      */
688 |     computeAngle: function(a, b) {
689 |         return (Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI);
690 |     },
691 | 
692 |     /**
693 |        Returns slope (Ax+B) between two points.
694 |         @param {L.Point} a Coordinates of point A
695 |         @param {L.Point} b Coordinates of point B
696 |         @returns {Object} with ``a`` and ``b`` properties.
697 |      */
698 |     computeSlope: function(a, b) {
699 |         var s = (b.y - a.y) / (b.x - a.x),
700 |             o = a.y - (s * a.x);
701 |         return {'a': s, 'b': o};
702 |     },
703 | 
704 |     /**
705 |        Returns LatLng of rotated point around specified LatLng center.
706 |         @param {L.LatLng} latlngPoint: point to rotate
707 |         @param {double} angleDeg: angle to rotate in degrees
708 |         @param {L.LatLng} latlngCenter: center of rotation
709 |         @returns {L.LatLng} rotated point
710 |      */
711 |     rotatePoint: function(map, latlngPoint, angleDeg, latlngCenter) {
712 |         var maxzoom = map.getMaxZoom();
713 |         if (maxzoom === Infinity)
714 |             maxzoom = map.getZoom();
715 |         var angleRad = angleDeg*Math.PI/180,
716 |             pPoint = map.project(latlngPoint, maxzoom),
717 |             pCenter = map.project(latlngCenter, maxzoom),
718 |             x2 = Math.cos(angleRad)*(pPoint.x-pCenter.x) - Math.sin(angleRad)*(pPoint.y-pCenter.y) + pCenter.x,
719 |             y2 = Math.sin(angleRad)*(pPoint.x-pCenter.x) + Math.cos(angleRad)*(pPoint.y-pCenter.y) + pCenter.y;
720 |         return map.unproject(new L.Point(x2,y2), maxzoom);
721 |     },
722 | 
723 |     /**
724 |        Returns the bearing in degrees clockwise from north (0 degrees)
725 |        from the first L.LatLng to the second, at the first LatLng
726 |        @param {L.LatLng} latlng1: origin point of the bearing
727 |        @param {L.LatLng} latlng2: destination point of the bearing
728 |        @returns {float} degrees clockwise from north.
729 |     */
730 |     bearing: function(latlng1, latlng2) {
731 |         var rad = Math.PI / 180,
732 |             lat1 = latlng1.lat * rad,
733 |             lat2 = latlng2.lat * rad,
734 |             lon1 = latlng1.lng * rad,
735 |             lon2 = latlng2.lng * rad,
736 |             y = Math.sin(lon2 - lon1) * Math.cos(lat2),
737 |             x = Math.cos(lat1) * Math.sin(lat2) -
738 |                 Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1);
739 | 
740 |         var bearing = ((Math.atan2(y, x) * 180 / Math.PI) + 360) % 360;
741 |         return bearing >= 180 ? bearing-360 : bearing;
742 |     },
743 | 
744 |     /**
745 |        Returns the point that is a distance and heading away from
746 |        the given origin point.
747 |        @param {L.LatLng} latlng: origin point
748 |        @param {float} heading: heading in degrees, clockwise from 0 degrees north.
749 |        @param {float} distance: distance in meters
750 |        @returns {L.latLng} the destination point.
751 |        Many thanks to Chris Veness at http://www.movable-type.co.uk/scripts/latlong.html
752 |        for a great reference and examples.
753 |     */
754 |     destination: function(latlng, heading, distance) {
755 |         heading = (heading + 360) % 360;
756 |         var rad = Math.PI / 180,
757 |             radInv = 180 / Math.PI,
758 |             R = 6378137, // approximation of Earth's radius
759 |             lon1 = latlng.lng * rad,
760 |             lat1 = latlng.lat * rad,
761 |             rheading = heading * rad,
762 |             sinLat1 = Math.sin(lat1),
763 |             cosLat1 = Math.cos(lat1),
764 |             cosDistR = Math.cos(distance / R),
765 |             sinDistR = Math.sin(distance / R),
766 |             lat2 = Math.asin(sinLat1 * cosDistR + cosLat1 *
767 |                 sinDistR * Math.cos(rheading)),
768 |             lon2 = lon1 + Math.atan2(Math.sin(rheading) * sinDistR *
769 |                 cosLat1, cosDistR - sinLat1 * Math.sin(lat2));
770 |         lon2 = lon2 * radInv;
771 |         lon2 = lon2 > 180 ? lon2 - 360 : lon2 < -180 ? lon2 + 360 : lon2;
772 |         return L.latLng([lat2 * radInv, lon2]);
773 |     },
774 | 
775 |     /**
776 |        Returns the the angle of the given segment and the Equator in degrees,
777 |        clockwise from 0 degrees north.
778 |        @param {L.Map} map: Leaflet map to be used for this method
779 |        @param {L.LatLng} latlngA: geographical point A of the segment
780 |        @param {L.LatLng} latlngB: geographical point B of the segment
781 |        @returns {Float} the angle in degrees.
782 |     */
783 |     angle: function(map, latlngA, latlngB) {
784 |       var pointA = map.latLngToContainerPoint(latlngA),
785 |           pointB = map.latLngToContainerPoint(latlngB),
786 |           angleDeg = Math.atan2(pointB.y - pointA.y, pointB.x - pointA.x) * 180 / Math.PI + 90;
787 |       angleDeg += angleDeg < 0 ? 360 : 0;
788 |       return angleDeg;
789 |     },
790 | 
791 |     /**
792 |        Returns a point snaps on the segment and heading away from the given origin point a distance.
793 |        @param {L.Map} map: Leaflet map to be used for this method
794 |        @param {L.LatLng} latlngA: geographical point A of the segment
795 |        @param {L.LatLng} latlngB: geographical point B of the segment
796 |        @param {float} distance: distance in meters
797 |        @returns {L.latLng} the destination point.
798 |     */
799 |     destinationOnSegment: function(map, latlngA, latlngB, distance) {
800 |       var angleDeg = L.GeometryUtil.angle(map, latlngA, latlngB),
801 |           latlng = L.GeometryUtil.destination(latlngA, angleDeg, distance);
802 |       return L.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB);
803 |     },
804 | });
805 | 
806 | return L.GeometryUtil;
807 | 
808 | }));
809 | 
810 |
811 |
812 | 813 | 814 | 815 | 816 |
817 | 818 |
819 | 820 | 823 | 824 | 825 | 826 | 827 | 828 | -------------------------------------------------------------------------------- /docs/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (function() { 3 | var source = document.getElementsByClassName('prettyprint source linenums'); 4 | var i = 0; 5 | var lineNumber = 0; 6 | var lineId; 7 | var lines; 8 | var totalLines; 9 | var anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = 'line' + lineNumber; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /docs/scripts/prettify/Apache-License-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /docs/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /docs/scripts/prettify/prettify.js: -------------------------------------------------------------------------------- 1 | var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; 2 | (function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= 3 | [],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), 9 | l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, 10 | q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, 11 | q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, 12 | "");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), 13 | a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} 14 | for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], 18 | "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], 19 | H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], 20 | J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ 21 | I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), 22 | ["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", 23 | /^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), 24 | ["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", 25 | hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= 26 | !k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p code { 197 | font-size: 0.85em; 198 | } 199 | 200 | .readme table { 201 | margin-bottom: 1em; 202 | border-collapse: collapse; 203 | border-spacing: 0; 204 | } 205 | 206 | .readme table tr { 207 | background-color: #fff; 208 | border-top: 1px solid #ccc; 209 | } 210 | 211 | .readme table th, 212 | .readme table td { 213 | padding: 6px 13px; 214 | border: 1px solid #ddd; 215 | } 216 | 217 | .readme table tr:nth-child(2n) { 218 | background-color: #f8f8f8; 219 | } 220 | 221 | /** Nav **/ 222 | nav { 223 | float: left; 224 | display: block; 225 | width: 250px; 226 | background: #fff; 227 | overflow: auto; 228 | position: fixed; 229 | height: 100%; 230 | padding: 10px; 231 | border-right: 1px solid #eee; 232 | /* box-shadow: 0 0 3px rgba(0,0,0,0.1); */ 233 | } 234 | 235 | nav li { 236 | list-style: none; 237 | padding: 0; 238 | margin: 0; 239 | } 240 | 241 | .nav-heading { 242 | margin-top: 10px; 243 | font-weight: bold; 244 | } 245 | 246 | .nav-heading a { 247 | color: #888; 248 | font-size: 14px; 249 | display: inline-block; 250 | } 251 | 252 | .nav-item-type { 253 | /* margin-left: 5px; */ 254 | width: 18px; 255 | height: 18px; 256 | display: inline-block; 257 | text-align: center; 258 | border-radius: 0.2em; 259 | margin-right: 5px; 260 | font-weight: bold; 261 | line-height: 20px; 262 | font-size: 13px; 263 | } 264 | 265 | .type-function { 266 | background: #B3E5FC; 267 | color: #0288D1; 268 | } 269 | 270 | .type-class { 271 | background: #D1C4E9; 272 | color: #4527A0; 273 | } 274 | 275 | .type-member { 276 | background: #C8E6C9; 277 | color: #388E3C; 278 | } 279 | 280 | .type-module { 281 | background: #E1BEE7; 282 | color: #7B1FA2; 283 | } 284 | 285 | 286 | /** Footer **/ 287 | footer { 288 | color: hsl(0, 0%, 28%); 289 | margin-left: 250px; 290 | display: block; 291 | padding: 30px; 292 | font-style: italic; 293 | font-size: 90%; 294 | border-top: 1px solid #eee; 295 | } 296 | 297 | .ancestors { 298 | color: #999 299 | } 300 | 301 | .ancestors a { 302 | color: #999 !important; 303 | text-decoration: none; 304 | } 305 | 306 | .clear { 307 | clear: both 308 | } 309 | 310 | .important { 311 | font-weight: bold; 312 | color: #950B02; 313 | } 314 | 315 | .yes-def { 316 | text-indent: -1000px 317 | } 318 | 319 | .type-signature { 320 | color: #aaa 321 | } 322 | 323 | .name, .signature { 324 | font-family: Consolas, Monaco, 'Andale Mono', monospace 325 | } 326 | 327 | .details { 328 | margin-top: 14px; 329 | border-left: 2px solid #DDD; 330 | line-height: 30px; 331 | } 332 | 333 | .details dt { 334 | width: 120px; 335 | float: left; 336 | padding-left: 10px; 337 | } 338 | 339 | .details dd { 340 | margin-left: 70px 341 | } 342 | 343 | .details ul { 344 | margin: 0 345 | } 346 | 347 | .details ul { 348 | list-style-type: none 349 | } 350 | 351 | .details li { 352 | margin-left: 30px 353 | } 354 | 355 | .details pre.prettyprint { 356 | margin: 0 357 | } 358 | 359 | .details .object-value { 360 | padding-top: 0 361 | } 362 | 363 | .description { 364 | margin-bottom: 1em; 365 | margin-top: 1em; 366 | } 367 | 368 | .code-caption { 369 | font-style: italic; 370 | font-size: 107%; 371 | margin: 0; 372 | } 373 | 374 | .prettyprint { 375 | font-size: 13px; 376 | border: 1px solid #ddd; 377 | border-radius: 3px; 378 | box-shadow: 0 1px 3px hsla(0, 0%, 0%, 0.05); 379 | overflow: auto; 380 | } 381 | 382 | .prettyprint.source { 383 | width: inherit 384 | } 385 | 386 | .prettyprint code { 387 | font-size: 12px; 388 | line-height: 18px; 389 | display: block; 390 | background-color: #fff; 391 | color: #4D4E53; 392 | } 393 | 394 | .prettyprint code:empty:before { 395 | content: ''; 396 | } 397 | 398 | .prettyprint > code { 399 | padding: 15px 400 | } 401 | 402 | .prettyprint .linenums code { 403 | padding: 0 15px 404 | } 405 | 406 | .prettyprint .linenums li:first-of-type code { 407 | padding-top: 15px 408 | } 409 | 410 | .prettyprint code span.line { 411 | display: inline-block 412 | } 413 | 414 | .prettyprint.linenums { 415 | padding-left: 70px; 416 | -webkit-user-select: none; 417 | -moz-user-select: none; 418 | -ms-user-select: none; 419 | user-select: none; 420 | } 421 | 422 | .prettyprint.linenums ol { 423 | padding-left: 0 424 | } 425 | 426 | .prettyprint.linenums li { 427 | border-left: 3px #ddd solid 428 | } 429 | 430 | .prettyprint.linenums li.selected, .prettyprint.linenums li.selected * { 431 | background-color: lightyellow 432 | } 433 | 434 | .prettyprint.linenums li * { 435 | -webkit-user-select: text; 436 | -moz-user-select: text; 437 | -ms-user-select: text; 438 | user-select: text; 439 | } 440 | 441 | .params, .props { 442 | border-spacing: 0; 443 | border: 1px solid #ddd; 444 | border-collapse: collapse; 445 | border-radius: 3px; 446 | box-shadow: 0 1px 3px rgba(0,0,0,0.1); 447 | width: 100%; 448 | font-size: 14px; 449 | /* margin-left: 15px; */ 450 | } 451 | 452 | .params .name, .props .name, .name code { 453 | color: #4D4E53; 454 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 455 | font-size: 100%; 456 | } 457 | 458 | .params td, .params th, .props td, .props th { 459 | margin: 0px; 460 | text-align: left; 461 | vertical-align: top; 462 | padding: 10px; 463 | display: table-cell; 464 | } 465 | 466 | .params td { 467 | border-top: 1px solid #eee 468 | } 469 | 470 | .params thead tr, .props thead tr { 471 | background-color: #fff; 472 | font-weight: bold; 473 | } 474 | 475 | .params .params thead tr, .props .props thead tr { 476 | background-color: #fff; 477 | font-weight: bold; 478 | } 479 | 480 | .params td.description > p:first-child, .props td.description > p:first-child { 481 | margin-top: 0; 482 | padding-top: 0; 483 | } 484 | 485 | .params td.description > p:last-child, .props td.description > p:last-child { 486 | margin-bottom: 0; 487 | padding-bottom: 0; 488 | } 489 | 490 | dl.param-type { 491 | /* border-bottom: 1px solid hsl(0, 0%, 87%); */ 492 | margin: 0; 493 | padding: 0; 494 | font-size: 16px; 495 | } 496 | 497 | .param-type dt, .param-type dd { 498 | display: inline-block 499 | } 500 | 501 | .param-type dd { 502 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 503 | display: inline-block; 504 | padding: 0; 505 | margin: 0; 506 | font-size: 14px; 507 | } 508 | 509 | .disabled { 510 | color: #454545 511 | } 512 | 513 | /* navicon button */ 514 | .navicon-button { 515 | display: none; 516 | position: relative; 517 | padding: 2.0625rem 1.5rem; 518 | transition: 0.25s; 519 | cursor: pointer; 520 | user-select: none; 521 | opacity: .8; 522 | } 523 | .navicon-button .navicon:before, .navicon-button .navicon:after { 524 | transition: 0.25s; 525 | } 526 | .navicon-button:hover { 527 | transition: 0.5s; 528 | opacity: 1; 529 | } 530 | .navicon-button:hover .navicon:before, .navicon-button:hover .navicon:after { 531 | transition: 0.25s; 532 | } 533 | .navicon-button:hover .navicon:before { 534 | top: .825rem; 535 | } 536 | .navicon-button:hover .navicon:after { 537 | top: -.825rem; 538 | } 539 | 540 | /* navicon */ 541 | .navicon { 542 | position: relative; 543 | width: 2.5em; 544 | height: .3125rem; 545 | background: #000; 546 | transition: 0.3s; 547 | border-radius: 2.5rem; 548 | } 549 | .navicon:before, .navicon:after { 550 | display: block; 551 | content: ""; 552 | height: .3125rem; 553 | width: 2.5rem; 554 | background: #000; 555 | position: absolute; 556 | z-index: -1; 557 | transition: 0.3s 0.25s; 558 | border-radius: 1rem; 559 | } 560 | .navicon:before { 561 | top: .625rem; 562 | } 563 | .navicon:after { 564 | top: -.625rem; 565 | } 566 | 567 | /* open */ 568 | .nav-trigger:checked + label:not(.steps) .navicon:before, 569 | .nav-trigger:checked + label:not(.steps) .navicon:after { 570 | top: 0 !important; 571 | } 572 | 573 | .nav-trigger:checked + label .navicon:before, 574 | .nav-trigger:checked + label .navicon:after { 575 | transition: 0.5s; 576 | } 577 | 578 | /* Minus */ 579 | .nav-trigger:checked + label { 580 | transform: scale(0.75); 581 | } 582 | 583 | /* × and + */ 584 | .nav-trigger:checked + label.plus .navicon, 585 | .nav-trigger:checked + label.x .navicon { 586 | background: transparent; 587 | } 588 | 589 | .nav-trigger:checked + label.plus .navicon:before, 590 | .nav-trigger:checked + label.x .navicon:before { 591 | transform: rotate(-45deg); 592 | background: #FFF; 593 | } 594 | 595 | .nav-trigger:checked + label.plus .navicon:after, 596 | .nav-trigger:checked + label.x .navicon:after { 597 | transform: rotate(45deg); 598 | background: #FFF; 599 | } 600 | 601 | .nav-trigger:checked + label.plus { 602 | transform: scale(0.75) rotate(45deg); 603 | } 604 | 605 | .nav-trigger:checked ~ nav { 606 | left: 0 !important; 607 | } 608 | 609 | .nav-trigger:checked ~ .overlay { 610 | display: block; 611 | } 612 | 613 | .nav-trigger { 614 | position: fixed; 615 | top: 0; 616 | clip: rect(0, 0, 0, 0); 617 | } 618 | 619 | .overlay { 620 | display: none; 621 | position: fixed; 622 | top: 0; 623 | bottom: 0; 624 | left: 0; 625 | right: 0; 626 | width: 100%; 627 | height: 100%; 628 | background: hsla(0, 0%, 0%, 0.5); 629 | z-index: 1; 630 | } 631 | 632 | .section-method { 633 | margin-bottom: 30px; 634 | padding-bottom: 30px; 635 | border-bottom: 1px solid #eee; 636 | } 637 | 638 | @media only screen and (min-width: 320px) and (max-width: 680px) { 639 | body { 640 | overflow-x: hidden; 641 | } 642 | 643 | nav { 644 | background: #FFF; 645 | width: 250px; 646 | height: 100%; 647 | position: fixed; 648 | top: 0; 649 | right: 0; 650 | bottom: 0; 651 | left: -250px; 652 | z-index: 3; 653 | padding: 0 10px; 654 | transition: left 0.2s; 655 | } 656 | 657 | .navicon-button { 658 | display: inline-block; 659 | position: fixed; 660 | top: 1.5em; 661 | right: 0; 662 | z-index: 2; 663 | } 664 | 665 | #main { 666 | width: 100%; 667 | min-width: 360px; 668 | } 669 | 670 | #main h1.page-title { 671 | margin: 1em 0; 672 | } 673 | 674 | #main section { 675 | padding: 0; 676 | } 677 | 678 | footer { 679 | margin-left: 0; 680 | } 681 | } 682 | 683 | @media only print { 684 | nav { 685 | display: none; 686 | } 687 | 688 | #main { 689 | float: none; 690 | width: 100%; 691 | } 692 | } 693 | -------------------------------------------------------------------------------- /docs/styles/prettify-jsdoc.css: -------------------------------------------------------------------------------- 1 | /* JSDoc prettify.js theme */ 2 | 3 | /* plain text */ 4 | .pln { 5 | color: #000000; 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | /* string content */ 11 | .str { 12 | color: hsl(104, 100%, 24%); 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | /* a keyword */ 18 | .kwd { 19 | color: #000000; 20 | font-weight: bold; 21 | font-style: normal; 22 | } 23 | 24 | /* a comment */ 25 | .com { 26 | font-weight: normal; 27 | font-style: italic; 28 | } 29 | 30 | /* a type name */ 31 | .typ { 32 | color: #000000; 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | /* a literal value */ 38 | .lit { 39 | color: #006400; 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | 44 | /* punctuation */ 45 | .pun { 46 | color: #000000; 47 | font-weight: bold; 48 | font-style: normal; 49 | } 50 | 51 | /* lisp open bracket */ 52 | .opn { 53 | color: #000000; 54 | font-weight: bold; 55 | font-style: normal; 56 | } 57 | 58 | /* lisp close bracket */ 59 | .clo { 60 | color: #000000; 61 | font-weight: bold; 62 | font-style: normal; 63 | } 64 | 65 | /* a markup tag name */ 66 | .tag { 67 | color: #006400; 68 | font-weight: normal; 69 | font-style: normal; 70 | } 71 | 72 | /* a markup attribute name */ 73 | .atn { 74 | color: #006400; 75 | font-weight: normal; 76 | font-style: normal; 77 | } 78 | 79 | /* a markup attribute value */ 80 | .atv { 81 | color: #006400; 82 | font-weight: normal; 83 | font-style: normal; 84 | } 85 | 86 | /* a declaration */ 87 | .dec { 88 | color: #000000; 89 | font-weight: bold; 90 | font-style: normal; 91 | } 92 | 93 | /* a variable name */ 94 | .var { 95 | color: #000000; 96 | font-weight: normal; 97 | font-style: normal; 98 | } 99 | 100 | /* a function name */ 101 | .fun { 102 | color: #000000; 103 | font-weight: bold; 104 | font-style: normal; 105 | } 106 | 107 | /* Specify class=linenums on a pre to get line numbering */ 108 | ol.linenums { 109 | margin-top: 0; 110 | margin-bottom: 0; 111 | } 112 | -------------------------------------------------------------------------------- /docs/styles/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: hsl(104, 100%, 24%); } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: hsl(240, 100%, 50%); } 17 | 18 | /* a comment */ 19 | .com { 20 | color: hsl(0, 0%, 60%); } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: hsl(240, 100%, 32%); } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: hsl(240, 100%, 40%); } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #000000; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #000000; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #000000; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /docs/tutorial-closest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | closest - Documentation 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 29 | 30 |
31 | 32 |

closest

33 | 34 | 35 |
36 | 37 |
38 | 39 |
40 | 41 |
42 | 43 | 44 | 45 | Demo of Leaflet.GeometryUtil distance and length 46 | 47 | 48 | 50 | 51 | 52 |
53 | 54 |
55 | 56 |
57 | 58 |
59 | 60 |
61 | Generated by JSDoc 3.6.3 on Thu Feb 13 2020 16:26:08 GMT+0100 (GMT+01:00) using the Minami theme. 62 |
63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /docs/tutorial-distance-length.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | distance-length - Documentation 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 29 | 30 |
31 | 32 |

distance-length

33 | 34 | 35 |
36 | 37 |
38 | 39 |
40 | 41 |
42 | 43 | 44 | 45 | Demo of Leaflet.GeometryUtil distance and length 46 | 47 | 48 | 50 | 51 | 52 | 53 |
54 | 55 |
56 | 57 |
58 | 59 |
60 | 61 |
62 | Generated by JSDoc 3.6.3 on Thu Feb 13 2020 16:26:08 GMT+0100 (GMT+01:00) using the Minami theme. 63 |
64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /jsdoc.config: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true, 4 | "dictionaries": ["jsdoc"] 5 | }, 6 | "source": { 7 | "include": ["README.md", "src/"], 8 | "includePattern": ".js$", 9 | "excludePattern": "(node_modules/|docs)" 10 | }, 11 | "plugins": [ 12 | "plugins/markdown" 13 | ], 14 | "templates": { 15 | "cleverLinks": false, 16 | "monospaceLinks": true, 17 | "useLongnameInNav": false 18 | }, 19 | "opts": { 20 | "encoding": "utf8", 21 | "template": "./node_modules/minami", 22 | "tutorials": "./tutorials" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet-geometryutil", 3 | "version": "0.10.3", 4 | "description": "Leaflet utility functions on geometries", 5 | "keywords": [ 6 | "Leaflet", 7 | "GIS" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/makinacorpus/Leaflet.GeometryUtil.git" 12 | }, 13 | "license": "BSD-3-Clause", 14 | "main": "src/leaflet.geometryutil.js", 15 | "types": "src/leaflet.geometryutil.d.ts", 16 | "scripts": { 17 | "test-leaflet-0.7.7": "npm install leaflet@0.7.7 --no-save && node_modules/.bin/mocha-chrome spec/index.html", 18 | "test-leaflet-1.0.0": "npm install leaflet@^1.0.0 --no-save && node_modules/.bin/mocha-chrome spec/index.html", 19 | "test-leaflet-1.6.0": "npm install leaflet@^1.6.0 --no-save && node_modules/.bin/mocha-chrome spec/index.html", 20 | "test": "npm run test-leaflet-0.7.7 && npm run test-leaflet-1.0.0 && npm run test-leaflet-1.6.0", 21 | "generate-docs": "rm -rf docs; node_modules/.bin/jsdoc -c jsdoc.config -d ./docs/" 22 | }, 23 | "dependencies": { 24 | "leaflet": "^1.6.0" 25 | }, 26 | "devDependencies": { 27 | "@types/leaflet": "^1.5.21", 28 | "chai": "3.5.0", 29 | "jsdoc": "^3.6.3", 30 | "minami": "^1.1.1", 31 | "mocha": "^7.0.1", 32 | "mocha-chrome": "^2.2.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /spec/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha Tests 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /spec/test.accumulatedLengths.js: -------------------------------------------------------------------------------- 1 | describe('Accumulated length of line', function() { 2 | it('It should be empty for empty line', function(done) { 3 | assert.deepEqual([], L.GeometryUtil.accumulatedLengths([])); 4 | done(); 5 | }); 6 | 7 | it('It should return 0 and length in meters for a segment', function(done) { 8 | var accumulatedLengths = L.GeometryUtil.accumulatedLengths(L.polyline([[0, 0], [1, 0]])); 9 | assert.equal(accumulatedLengths[0], 0); 10 | assert.closeTo(accumulatedLengths[1], 111319.49079327357, 500); // compatibility of Leaflet 1.0, due to earth R changed 11 | done(); 12 | }); 13 | 14 | it('It should return accumulated lengths', function(done) { 15 | var accumulatedLengths = L.GeometryUtil.accumulatedLengths(L.polyline([[0, 0], [0.5, 0], [1, 0]])); 16 | assert.equal(accumulatedLengths[0], 0); 17 | assert.closeTo(accumulatedLengths[1], 55659.74539663678, 500); // compatibility of Leaflet 1.0, due to earth R changed 18 | assert.closeTo(accumulatedLengths[2], 111319.49079327357, 500); // compatibility of Leaflet 1.0, due to earth R changed 19 | done(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /spec/test.angle.js: -------------------------------------------------------------------------------- 1 | describe('Angle', function() { 2 | 3 | it('It should be 45 degrees', function(done) { 4 | var latlng1 = L.latLng([0.0, 0.0]), 5 | latlng2 = L.latLng([1.0, 1.0]), 6 | result = 45.0; 7 | assert.equal(result, L.GeometryUtil.angle(map, latlng1, latlng2)); 8 | done(); 9 | }); 10 | 11 | it('It should be degrees clockwise from east, 0 degrees', function(done) { 12 | var latlng1 = L.latLng([0.0, 0.0]), 13 | latlng2 = L.latLng([1.0, 0.0]), 14 | result = 0.0; 15 | assert.equal(result, L.GeometryUtil.angle(map, latlng1, latlng2)); 16 | done(); 17 | }); 18 | 19 | it('It should not be a negative value when the angle is greater than 180', function(done) { 20 | var latlng1 = L.latLng([0.0, 0.0]), 21 | latlng2 = L.latLng([-1.0, 1.0]), 22 | result = -135.0; 23 | assert.notEqual(result, L.GeometryUtil.angle(map, latlng1, latlng2)); 24 | done(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /spec/test.bearing.js: -------------------------------------------------------------------------------- 1 | describe('Compute Bearing', function() { 2 | 3 | it('It should be degrees clockwise from north, 0 degrees.', function(done) { 4 | var latlng1 = L.latLng([0.0, 0.0]), 5 | latlng2 = L.latLng([90.0, 0.0]); 6 | assert.equal(0.0, L.GeometryUtil.bearing(latlng1,latlng2)); 7 | done(); 8 | }); 9 | 10 | it('Same point, should be zero.', function(done) { 11 | var latlng1 = L.latLng([0.0, 0.0]), 12 | latlng2 = L.latLng([0.0, 0.0]); 13 | assert.equal(0, L.GeometryUtil.bearing(latlng1,latlng2)); 14 | done(); 15 | }); 16 | 17 | it('Crossing Prime Meridian.', function(done) { 18 | var latlng1 = L.latLng([10.0, -10.0]), 19 | latlng2 = L.latLng([-10.0, 10.0]); 20 | assert.equal(134.5614514132577, L.GeometryUtil.bearing(latlng1,latlng2)); 21 | done(); 22 | }); 23 | 24 | it('Negative value for bearing greater than / equal to 180', function(done) { 25 | var latlng1 = L.latLng([33.0, -120.0]), 26 | latlng2 = L.latLng([34.0, -122.0]); 27 | assert.equal(-58.503883697887375, L.GeometryUtil.bearing(latlng1,latlng2)); 28 | done(); 29 | }); 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /spec/test.closest.js: -------------------------------------------------------------------------------- 1 | describe('Closest on path with precision', function() { 2 | it('It should have distance at 0 if on path', function(done) { 3 | var ll = L.latLng([0, 0]), 4 | closest = L.GeometryUtil.closest(map, [[-30, -50], [-10, -10], [10, 10], [30, 50]], ll); 5 | assert.equal(0, closest.distance); 6 | assert.equal(ll.toString(), closest.toString()); 7 | done(); 8 | }); 9 | 10 | it('It should return same point if on path', function(done) { 11 | var line = L.polyline([[0,0], [1, 1], [2, 2]]); 12 | closest = L.GeometryUtil.closest(map, line, [1.7, 1.7]); 13 | assert.almostEqual(closest.lat, 1.7, 2); 14 | assert.almostEqual(closest.lng, 1.7, 2); 15 | done(); 16 | }); 17 | 18 | it('It should be exactly on path', function(done) { 19 | var ll = L.latLng([1, -1]), 20 | closest = L.GeometryUtil.closest(map, [[-10, -10], [10, 10]], ll); 21 | assert.equal(Math.sqrt(2), closest.distance); 22 | // TODO: should not be almost equal 23 | assert.almostEqual(closest.lat, 0, 2); 24 | assert.almostEqual(closest.lng, 0, 2); 25 | done(); 26 | }); 27 | 28 | it('It should not depend on zoom', function(done) { 29 | // Test with plain value 30 | var ll = L.latLng([5, 10]), 31 | line = L.polyline([[-50, -10], [30, 40]])/*.addTo(map)*/, 32 | closest = L.GeometryUtil.closest(map, line, ll); 33 | assert.isTrue(closest.distance > 0); 34 | /* 35 | SELECT ST_AsText( 36 | ST_ClosestPoint( 37 | ST_MakeLine('SRID=4326;POINT(-10 -50)'::geometry, 'SRID=4326;POINT(40 30)'::geometry), 38 | 'SRID=4326;POINT(10 5)'::geometry)) 39 | Gives: 40 | "POINT(20.3370786516854 -1.46067415730337)" 41 | TODO: find out what's going on with Longitudes :) 42 | */ 43 | assert.almostEqual(closest.lat, -1.467431, 6) 44 | assert.almostEqual(closest.lng, 21.5729367, 6) 45 | 46 | // Change zoom and check that closest did not change. 47 | assert.equal(0, map.getZoom()); 48 | L.Util.setOptions(map, {maxZoom: 18}); 49 | 50 | map.on('moveend', function () { 51 | assert.notEqual(0, map.getZoom()); 52 | 53 | closest = L.GeometryUtil.closest(map, line, ll); 54 | assert.almostEqual(closest.lat, -1.467431, 6) 55 | assert.almostEqual(closest.lng, 21.5729367, 6) 56 | // Restore zoom 57 | map.off('moveend'); 58 | map._resetView(map.getCenter(), 0); 59 | done(); 60 | }); 61 | 62 | map._resetView(map.getCenter(), 17); 63 | }); 64 | 65 | it('It should work with last segment of polygon', function(done) { 66 | var polygon = L.polygon([[0, 0], [10, 10], [0, 10]])/*.addTo(map)*/, 67 | ll = [0, 5], 68 | marker = L.marker(ll)/*.addTo(map)*/, 69 | closest = L.GeometryUtil.closest(map, polygon, ll); 70 | assert.almostEqual(closest.lat, 0, 2); 71 | assert.almostEqual(closest.lng, 5, 2); 72 | done(); 73 | }); 74 | 75 | it('It should not alterate the latLngs of a polygon', function(done) { 76 | var polygon = L.polygon([[0, 0], [10, 10], [0, 10]])/*.addTo(map)*/, 77 | ll = [0, 5], 78 | marker = L.marker(ll), 79 | latlngs; 80 | 81 | latlngs = polygon.getLatLngs(); 82 | if (L.Polyline._flat(latlngs)) { 83 | assert.equal(latlngs.length, 3) 84 | } else { 85 | assert.equal(latlngs.length, 1) 86 | assert.equal(latlngs[0].length, 3) 87 | } 88 | 89 | closest = L.GeometryUtil.closest(map, polygon, ll); 90 | 91 | latlngs = polygon.getLatLngs(); 92 | if (L.Polyline._flat(latlngs)) { 93 | assert.equal(latlngs.length, 3) 94 | } else { 95 | assert.equal(latlngs.length, 1) 96 | assert.equal(latlngs[0].length, 3) 97 | } 98 | 99 | done(); 100 | }); 101 | 102 | it('It should return null if layer param is not instance of Array|L.Polygon|L.Polyline (Leaflet 0.7.7 only)', function(done) { 103 | var campus = { 104 | "type": "Feature", 105 | "properties": { 106 | "popupContent": "This is the Auraria West Campus", 107 | "style": { 108 | weight: 2, 109 | color: "#999", 110 | opacity: 1, 111 | fillColor: "#B0DE5C", 112 | fillOpacity: 0.8 113 | } 114 | }, 115 | "geometry": { 116 | "type": "MultiPolygon", 117 | "coordinates": [ 118 | [ 119 | [ 120 | [-105.00432014465332, 39.74732195489861], 121 | [-105.00715255737305, 39.74620006835170], 122 | [-105.00921249389647, 39.74468219277038], 123 | [-105.01067161560059, 39.74362625960105], 124 | [-105.01195907592773, 39.74290029616054], 125 | [-105.00989913940431, 39.74078835902781], 126 | [-105.00758171081543, 39.74059036160317], 127 | [-105.00346183776855, 39.74059036160317], 128 | [-105.00097274780272, 39.74059036160317], 129 | [-105.00062942504881, 39.74072235994946], 130 | [-105.00020027160645, 39.74191033368865], 131 | [-105.00071525573731, 39.74276830198601], 132 | [-105.00097274780272, 39.74369225589818], 133 | [-105.00097274780272, 39.74461619742136], 134 | [-105.00123023986816, 39.74534214278395], 135 | [-105.00183105468751, 39.74613407445653], 136 | [-105.00432014465332, 39.74732195489861] 137 | ],[ 138 | [-105.00361204147337, 39.74354376414072], 139 | [-105.00301122665405, 39.74278480127163], 140 | [-105.00221729278564, 39.74316428375108], 141 | [-105.00283956527711, 39.74390674342741], 142 | [-105.00361204147337, 39.74354376414072] 143 | ] 144 | ],[ 145 | [ 146 | [-105.00942707061768, 39.73989736613708], 147 | [-105.00942707061768, 39.73910536278566], 148 | [-105.00685214996338, 39.73923736397631], 149 | [-105.00384807586671, 39.73910536278566], 150 | [-105.00174522399902, 39.73903936209552], 151 | [-105.00041484832764, 39.73910536278566], 152 | [-105.00041484832764, 39.73979836621592], 153 | [-105.00535011291504, 39.73986436617916], 154 | [-105.00942707061768, 39.73989736613708] 155 | ] 156 | ] 157 | ] 158 | } 159 | }, 160 | layers = L.geoJson(campus)/*.addTo(map)*/, 161 | ll = [-1, 5], 162 | marker = L.marker(ll)/*.addTo(map)*/, 163 | closest = L.GeometryUtil.closest(map, layers.getLayers()[0], ll); 164 | // if layers.getLayers()[0] is a LayerGroup, we are in Leaflet 0.7.7 165 | // so there is no result 166 | // if not, we are in Leaflet 1.0, and we don't need to test it, because 167 | // layers.getLayers()[0] will contain a multipolygon, and so there is a result 168 | if (layers.getLayers()[0] instanceof L.LayerGroup) { 169 | assert.isNull(closest); 170 | } else { 171 | assert.isNotNull(closest); 172 | } 173 | done(); 174 | }); 175 | 176 | it('It should have distance at 0 if on path of a nested array', function(done) { 177 | var ll = L.latLng([0, 0]), 178 | closest = L.GeometryUtil.closest(map, [ [[-30, -50], [-10, -10], [10, 10], [30, 50]], [[-10, -20], [-30, -10], [40, 10], [50, 50]] ], ll); 179 | assert.equal(0, closest.distance); 180 | assert.equal(ll.toString(), closest.toString()); 181 | done(); 182 | }); 183 | 184 | it('It should work with nested arrays and return the correct point', function(done) { 185 | var closest = L.GeometryUtil.closest(map, [ [[0,0], [1, 1], [2, 2]], [[0,0], [2, 2], [4, 4]] ], [3, 3]); 186 | assert.latLngEqual(closest, L.latLng(3, 3)); 187 | done(); 188 | }); 189 | 190 | it('It should work with nested arrays and last segment of polygon', function(done) { 191 | var campus = { 192 | "type": "Feature", 193 | "geometry": { 194 | "type": "MultiPolygon", 195 | "coordinates": [ 196 | [ 197 | [ 198 | [0, 0], [50, 50], [100, 0] 199 | ], 200 | [ 201 | [20,10], [50, 40], [80, 10] 202 | ] 203 | ] 204 | ] 205 | } 206 | }, 207 | layers = L.geoJson(campus)/*.addTo(map)*/, 208 | ll = L.latLng([0, 50]), 209 | closest = L.GeometryUtil.closest(map, layers.getLayers()[0], ll); 210 | // if layers.getLayers()[0] is a LayerGroup, we are in Leaflet 0.7.7 211 | // so there is no result 212 | if (layers.getLayers()[0] instanceof L.LayerGroup) { 213 | assert.isNull(closest); 214 | } else { 215 | assert.latLngEqual(L.latLng(0,2), closest); 216 | assert.equal(closest.distance, 0); 217 | } 218 | done(); 219 | }); 220 | 221 | it('It must not return a point on a segment between the last point of a polygon and the first point of his follower', function(done) { 222 | var campus = { 223 | "type": "Feature", 224 | "geometry": { 225 | "type": "MultiPolygon", 226 | "coordinates": [ 227 | [ 228 | [ 229 | [0, 0], [50, 50], [100, 0] 230 | ], 231 | [ 232 | [20,10], [50, 40], [80, 10] 233 | ] 234 | ] 235 | ] 236 | } 237 | }, 238 | layers = L.geoJson(campus)/*.addTo(map)*/, 239 | ll = L.latLng(5,10), 240 | marker = L.marker(ll)/*.addTo(map)*/, 241 | closest = L.GeometryUtil.closest(map, layers.getLayers()[0], ll); 242 | // if layers.getLayers()[0] is a LayerGroup, we are in Leaflet 0.7.7 243 | // so there is no result 244 | if (layers.getLayers()[0] instanceof L.LayerGroup) { 245 | assert.isNull(closest); 246 | } else { 247 | // L.marker(closest).addTo(map) 248 | assert.almostNotEqual(ll.lat, closest.lat, .5); 249 | assert.almostNotEqual(ll.lng, closest.lng, .5); 250 | assert.almostEqual('7.8', closest.lat, .5); 251 | assert.almostEqual('6.75', closest.lng, .5); 252 | } 253 | done(); 254 | }); 255 | 256 | }); 257 | -------------------------------------------------------------------------------- /spec/test.closestCircle.js: -------------------------------------------------------------------------------- 1 | describe("Closest on circle", function () { 2 | it("It should be same point if is on the circle", function (done) { 3 | const center = L.latLng([0, 0]); 4 | const radius = 5; 5 | const point = L.latLng([0, 5]); 6 | const circle = new L.Circle(center, { radius }); 7 | const closest = L.GeometryUtil.closestOnCircle(circle, point); 8 | assert.equal(point.toString(), closest.toString()); 9 | done(); 10 | }); 11 | 12 | it("It should be closest if is inside the circle", function (done) { 13 | const center = L.latLng([0, 0]); 14 | const radius = 5; 15 | const point = L.latLng([0, 4]); 16 | const expected = L.latLng([0, 5]); 17 | const circle = new L.Circle(center, { radius }); 18 | const closest = L.GeometryUtil.closestOnCircle(circle, point); 19 | assert.equal(expected.toString(), closest.toString()); 20 | done(); 21 | }); 22 | 23 | it("It should be closest if is outside the circle to the right", function (done) { 24 | const center = L.latLng([0, 0]); 25 | const radius = 5; 26 | const point = L.latLng([0, 6]); 27 | const expected = L.latLng([0, 5]); 28 | const circle = new L.Circle(center, { radius }); 29 | const closest = L.GeometryUtil.closestOnCircle(circle, point); 30 | assert.equal(expected.toString(), closest.toString()); 31 | done(); 32 | }); 33 | 34 | it("It should be closest if is outside the circle to the left", function (done) { 35 | const center = L.latLng([0, 0]); 36 | const radius = 5; 37 | const point = L.latLng([0, -8]); 38 | const expected = L.latLng([0, -5]); 39 | const circle = new L.Circle(center, { radius }); 40 | const closest = L.GeometryUtil.closestOnCircle(circle, point); 41 | assert.equal(expected.toString(), closest.toString()); 42 | done(); 43 | }); 44 | 45 | it("It should be closest if is outside the circle downwards", function (done) { 46 | const center = L.latLng([0, 0]); 47 | const radius = 5; 48 | const point = L.latLng([-10, 0]); 49 | const expected = L.latLng([-5, 0]); 50 | const circle = new L.Circle(center, { radius }); 51 | const closest = L.GeometryUtil.closestOnCircle(circle, point); 52 | assert.equal(expected.toString(), closest.toString()); 53 | done(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /spec/test.closestLayer.js: -------------------------------------------------------------------------------- 1 | describe('Closest among layers', function() { 2 | it('It should return null if list is empty', function(done) { 3 | var ll = L.latLng([0, 0]), 4 | closest = L.GeometryUtil.closestLayer(map, [], ll); 5 | assert.equal(null, closest); 6 | done(); 7 | }); 8 | 9 | it('It should return an object with layer, latlng and distance', function(done) { 10 | var ll = L.latLng([0, 0]), 11 | layers = [L.marker([2, 2])], 12 | closest = L.GeometryUtil.closestLayer(map, layers, ll); 13 | assert.deepEqual(closest, 14 | {layer: layers[0], latlng: layers[0].getLatLng(), distance: Math.sqrt(2)}); 15 | done(); 16 | }); 17 | 18 | it('It should return the sub layer closest to the latlng point', function(done) { 19 | var ll = L.latLng([0,5]), 20 | campus = { 21 | "type": "Feature", 22 | "properties": { 23 | "popupContent": "This is the Auraria West Campus", 24 | "style": { 25 | weight: 2, 26 | color: "#999", 27 | opacity: 1, 28 | fillColor: "#B0DE5C", 29 | fillOpacity: 0.8 30 | } 31 | }, 32 | "geometry": { 33 | "type": "MultiPolygon", 34 | "coordinates": [ 35 | [ 36 | [ 37 | [0, 0], 38 | [0, 10], 39 | [-105.00921249389647, 39.74468219277038], 40 | [-105.01067161560059, 39.74362625960105], 41 | [-105.01195907592773, 39.74290029616054], 42 | [-105.00989913940431, 39.74078835902781], 43 | [-105.00758171081543, 39.74059036160317], 44 | [-105.00346183776855, 39.74059036160317], 45 | [-105.00097274780272, 39.74059036160317], 46 | [-105.00062942504881, 39.74072235994946], 47 | [-105.00020027160645, 39.74191033368865], 48 | [-105.00071525573731, 39.74276830198601], 49 | [-105.00097274780272, 39.74369225589818], 50 | [-105.00097274780272, 39.74461619742136], 51 | [-105.00123023986816, 39.74534214278395], 52 | [-105.00183105468751, 39.74613407445653], 53 | [-105.00432014465332, 39.74732195489861] 54 | ],[ 55 | [-105.00361204147337, 39.74354376414072], 56 | [-105.00301122665405, 39.74278480127163], 57 | [-105.00221729278564, 39.74316428375108], 58 | [-105.00283956527711, 39.74390674342741], 59 | [-105.00361204147337, 39.74354376414072] 60 | ] 61 | ],[ 62 | [ 63 | [-105.00942707061768, 39.73989736613708], 64 | [-105.00942707061768, 39.73910536278566], 65 | [-105.00685214996338, 39.73923736397631], 66 | [-105.00384807586671, 39.73910536278566], 67 | [-105.00174522399902, 39.73903936209552], 68 | [-105.00041484832764, 39.73910536278566], 69 | [-105.00041484832764, 39.73979836621592], 70 | [-105.00535011291504, 39.73986436617916], 71 | [-105.00942707061768, 39.73989736613708] 72 | ] 73 | ] 74 | ] 75 | } 76 | }, 77 | layers = L.geoJson(campus, { 78 | 79 | pointToLayer: function (feature, latlng) { 80 | return L.circleMarker(latlng, { 81 | radius: 8, 82 | fillColor: "#ff7800", 83 | color: "#000", 84 | weight: 1, 85 | opacity: 1, 86 | fillOpacity: 0.8 87 | }); 88 | } 89 | }), 90 | closest = L.GeometryUtil.closestLayer(map, layers.getLayers(), ll); 91 | // we test instanceof because of differences between Leaflet 0.7.7 & 1.0.0 92 | // in 0.7.7 geojson are LayerGroup, in 1.0, we have directly instances of L.Polyline & co 93 | assert.deepEqual(closest.layer, layers.getLayers()[0] instanceof L.LayerGroup ? layers.getLayers()[0].getLayers()[0] : layers.getLayers()[0]) 94 | assert.deepEqual(closest.distance, 4) 95 | done(); 96 | }) 97 | }); -------------------------------------------------------------------------------- /spec/test.closestLayerSnap.js: -------------------------------------------------------------------------------- 1 | describe('Closest snap', function() { 2 | var square, diagonal, d, w, layers; 3 | 4 | beforeEach(function() { 5 | // Snapping distance 6 | d = L.GeometryUtil.distance(map, L.latLng([0, 0]), L.latLng([0, 10])); 7 | w = 3 * d; 8 | square = L.rectangle([[-w, -w], [w, w]]); 9 | diagonal = L.polyline([[-w, -w], [0, 0], [w, w]]); 10 | layers = [square, diagonal]; 11 | }); 12 | 13 | it('It should snap even if over layer', function(done) { 14 | var snap = L.GeometryUtil.closestLayerSnap(map, layers, L.latLng([0, 0])); 15 | assert.equal(snap.distance, 0); 16 | assert.equal(snap.layer, diagonal); 17 | done(); 18 | }); 19 | 20 | it('It should not snap if tolerance exceeded', function(done) { 21 | var snap = L.GeometryUtil.closestLayerSnap(map, layers, L.latLng([-w-d, w+d]), d); 22 | assert.equal(null, snap); 23 | done(); 24 | }); 25 | 26 | it('It should snap to corners by default', function(done) { 27 | var snap = L.GeometryUtil.closestLayerSnap(map, layers, L.latLng([-w-d, w+d])); 28 | assert.isTrue(snap.distance > d); 29 | assert.equal(snap.layer, square); 30 | done(); 31 | }); 32 | 33 | it('It should not snap to corners if vertices disabled', function(done) { 34 | var corner = L.GeometryUtil.closestLayerSnap(map, layers, L.latLng([w-d, -w-d])); 35 | assert.equal(corner.layer, square); 36 | assert.almostEqual(corner.latlng.lat, w); 37 | assert.almostEqual(corner.latlng.lng, -w); 38 | 39 | var snap = L.GeometryUtil.closestLayerSnap(map, layers, L.latLng([w-d, -w-d]), Infinity, false); 40 | assert.almostEqual(snap.latlng.lat, w-d); 41 | assert.almostEqual(snap.latlng.lng, -w); 42 | done(); 43 | }); 44 | 45 | it('It should not snap to corners if distance to vertice exceeds tolerance', function(done) { 46 | var corner = L.GeometryUtil.closestLayerSnap(map, layers, L.latLng([w-d-d/2, -w-d])); 47 | assert.equal(corner.layer, square); 48 | assert.almostEqual(corner.latlng.lat, w); 49 | assert.almostEqual(corner.latlng.lng, -w); 50 | 51 | var snap = L.GeometryUtil.closestLayerSnap(map, layers, L.latLng([w-d-d/2, -w-d]), d); 52 | assert.almostEqual(snap.latlng.lat, w-d-d/2); 53 | assert.almostEqual(snap.latlng.lng, -w); 54 | done(); 55 | }); 56 | }); -------------------------------------------------------------------------------- /spec/test.closestSegment.js: -------------------------------------------------------------------------------- 1 | describe('Closest on segment', function() { 2 | it('It should be same point if point on segment', function(done) { 3 | var ll = L.latLng([0, 0]), 4 | closest = L.GeometryUtil.closestOnSegment(map, ll, L.latLng([0, 0]), L.latLng([10, 10])); 5 | assert.equal(ll.toString(), closest.toString()); 6 | done(); 7 | }); 8 | 9 | it('It should be exactly on path', function(done) { 10 | var ll = L.latLng([-1, 1]), 11 | closest = L.GeometryUtil.closestOnSegment(map, ll, L.latLng([-10, -10]), L.latLng([10, 10])); 12 | // TODO: should not be almost equal 13 | assert.almostEqual(0, closest.lat, 2); 14 | assert.almostEqual(0, closest.lng, 2); 15 | done(); 16 | }); 17 | }); -------------------------------------------------------------------------------- /spec/test.computeAngle.js: -------------------------------------------------------------------------------- 1 | describe('Compute angle', function() { 2 | it('It should return angle', function(done) { 3 | var p1 = L.point(0, 0), 4 | p2 = L.point(6, 6); 5 | assert.equal(L.GeometryUtil.computeAngle(p1, p2), 45); 6 | done(); 7 | }); 8 | }); -------------------------------------------------------------------------------- /spec/test.computeSlope.js: -------------------------------------------------------------------------------- 1 | describe('Compute slope', function() { 2 | it('It should return A and B', function(done) { 3 | var p1 = L.point(0, 2), 4 | p2 = L.point(5, 7); 5 | assert.deepEqual(L.GeometryUtil.computeSlope(p1, p2), {a: 1, b: 2}) 6 | done(); 7 | }); 8 | }); -------------------------------------------------------------------------------- /spec/test.destination.js: -------------------------------------------------------------------------------- 1 | describe('Destination', function() { 2 | 3 | var radius = L.CRS.Earth.R; 4 | 5 | it('It should be [90.0,0.0]', function(done) { 6 | var latlng1 = L.latLng([0.0, 0.0]), 7 | heading = 0.0; 8 | dist = radius * Math.PI / 2.0; // 1/4 Earth's circumference. 9 | result = L.latLng([90.0,0.0]); 10 | assert.latLngEqual(result, L.GeometryUtil.destination(latlng1, heading, dist)); 11 | done(); 12 | }); 13 | 14 | it('Crossing the International Date Line', function(done) { 15 | var latlng1 = L.latLng([0.0, -175.0]), 16 | heading = -90.0; 17 | dist = radius * Math.PI / 8.0; 18 | result = L.latLng([0.0, 162.5]); 19 | assert.latLngEqual(result, L.GeometryUtil.destination(latlng1, heading, dist)); 20 | done(); 21 | }); 22 | 23 | it('Crossing the Prime Meridian', function(done) { 24 | var latlng1 = L.latLng([10.0, -10.0]), 25 | heading = 134.5614514132577; 26 | dist = 3137041.1135971523; 27 | result = L.latLng([-10, 10.0]); 28 | assert.latLngEqual(result, L.GeometryUtil.destination(latlng1, heading, dist)); 29 | done(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /spec/test.distanceSegment.js: -------------------------------------------------------------------------------- 1 | 2 | describe('Distance to segment', function() { 3 | it('It should be 0 if point on segment', function(done) { 4 | assert.equal(0, L.GeometryUtil.distanceSegment(map, L.latLng([10, 5]), L.latLng([10, 0]), L.latLng([10, 10]))); 5 | done(); 6 | }); 7 | 8 | it('It should not fail if segment has no length', function(done) { 9 | assert.equal(1, L.GeometryUtil.distanceSegment(map, L.latLng([0, 1]), L.latLng([0, 0]), L.latLng([0, 0]))); 10 | done(); 11 | }); 12 | 13 | it('It should be the shortest distance', function(done) { 14 | assert.equal(1, L.GeometryUtil.distanceSegment(map, L.latLng([0, 1]), L.latLng([0, 0]), L.latLng([10, 0]))); 15 | done(); 16 | }); 17 | }); -------------------------------------------------------------------------------- /spec/test.extract.js: -------------------------------------------------------------------------------- 1 | describe('Extract line', function() { 2 | var line = L.polyline([[0,0], [1, 1], [2, 2], [3, 3]]); 3 | 4 | it('It should return all coordinates from 0 to 1', function(done) { 5 | assert.deepEqual(L.GeometryUtil.extract(map, line, 0, 1), line.getLatLngs()); 6 | done(); 7 | }); 8 | 9 | it('It should return inverted coordinates from 1 to 0', function(done) { 10 | assert.deepEqual(L.GeometryUtil.extract(map, line, 1, 0), L.GeometryUtil.reverse(line).getLatLngs()); 11 | done(); 12 | }); 13 | 14 | it('It should return one coordinate if start equals end', function(done) { 15 | assert.latLngEqual(L.latLng(0.7501691078194406, 0.7501524538236026), 16 | L.GeometryUtil.extract(map, line, 0.25, 0.25)[0]); 17 | done(); 18 | }); 19 | 20 | it('It should return extra coordinate if middle of segment', function(done) { 21 | 22 | var extract1 = L.GeometryUtil.extract(map, line, 0, 0.2), 23 | extract2 = L.GeometryUtil.extract(map, line, 0, 0.6), 24 | extract3 = L.GeometryUtil.extract(map, line, 0.6, 1.0), 25 | extract4 = L.GeometryUtil.extract(map, line, 0.2, 0.8), 26 | extract5 = L.GeometryUtil.extract(map, line, 1.0, 0.6); 27 | 28 | assert.deepEqual(extract1[0], L.latLng([0, 0])); 29 | assert.closeTo(extract1[1].lat, 0.600141459027052, 0.0000001); // compatibility of Leaflet 1.0, due to earth R changed 30 | assert.closeTo(extract1[1].lng, 0.6001219630588661, 0.0000001); // compatibility of Leaflet 1.0, due to earth R changed 31 | 32 | assert.deepEqual(extract2[0], L.latLng([0, 0])); 33 | assert.deepEqual(extract2[1], L.latLng([1, 1])); 34 | assert.closeTo(extract2[2].lat, 1.800282914111311, 0.0000001); // compatibility of Leaflet 1.0, due to earth R changed 35 | assert.closeTo(extract2[2].lng, 1.8002439493392906, 0.0000001); // compatibility of Leaflet 1.0, due to earth R changed 36 | 37 | assert.closeTo(extract3[0].lat, 1.800282914111311, 0.0000001); // compatibility of Leaflet 1.0, due to earth R changed 38 | assert.closeTo(extract3[0].lng, 1.8002439493392906, 0.0000001); // compatibility of Leaflet 1.0, due to earth R changed 39 | assert.deepEqual(extract3[1], L.latLng([2, 2])); 40 | assert.deepEqual(extract3[2], L.latLng([3, 3])); 41 | 42 | assert.closeTo(extract4[0].lat, 0.600141459027052, 0.0000001); // compatibility of Leaflet 1.0, due to earth R changed 43 | assert.closeTo(extract4[0].lng, 0.6001219630588661, 0.0000001); // compatibility of Leaflet 1.0, due to earth R changed 44 | assert.deepEqual(extract4[1], L.latLng([1, 1])); 45 | assert.deepEqual(extract4[2], L.latLng([2, 2])); 46 | assert.closeTo(extract4[3].lat, 2.40024267258436, 0.0000001); // compatibility of Leaflet 1.0, due to earth R changed 47 | assert.closeTo(extract4[3].lng, 2.4001524293923637, 0.0000001); // compatibility of Leaflet 1.0, due to earth R changed 48 | 49 | // Should work symetrically 50 | assert.deepEqual(extract5[0], L.latLng([3, 3])); 51 | assert.deepEqual(extract5[1], L.latLng([2, 2])); 52 | assert.closeTo(extract5[2].lat, 1.800282914111311, 0.0000001); // compatibility of Leaflet 1.0, due to earth R changed 53 | assert.closeTo(extract5[2].lng, 1.8002439493392906, 0.0000001); // compatibility of Leaflet 1.0, due to earth R changed 54 | 55 | done(); 56 | }); 57 | }); -------------------------------------------------------------------------------- /spec/test.interpolateOnLine.js: -------------------------------------------------------------------------------- 1 | describe('Interpolate on line', function() { 2 | var llA = L.latLng(1, 2), 3 | llB = L.latLng(3, 4), 4 | llC = L.latLng(5, 6); 5 | 6 | it('It should be null if the line has less than 2 vertices', function(done) { 7 | assert.equal(null, L.GeometryUtil.interpolateOnLine(map, [], 0.5)); 8 | assert.equal(null, L.GeometryUtil.interpolateOnLine(map, [llA], 0.5)); 9 | done(); 10 | }); 11 | 12 | 13 | it('It should be the first vertex if offset is 0', function(done) { 14 | var interp = L.GeometryUtil.interpolateOnLine(map, [llA, llB], 0); 15 | assert.latLngEqual(interp.latLng, llA); 16 | assert.equal(interp.predecessor, -1); 17 | done(); 18 | }); 19 | 20 | it('It should be the first vertex if offset is less than 0', function(done) { 21 | var interp = L.GeometryUtil.interpolateOnLine(map, [llA, llB], -10); 22 | assert.latLngEqual(interp.latLng, llA); 23 | assert.equal(interp.predecessor, -1); 24 | done(); 25 | }); 26 | 27 | it('It should be the last vertex if offset is 1', function(done) { 28 | var interp = L.GeometryUtil.interpolateOnLine(map, [llA, llB, llC], 1); 29 | assert.latLngEqual(interp.latLng, llC); 30 | assert.equal(interp.predecessor, 1); 31 | done(); 32 | }); 33 | 34 | it('It should be the last vertex if offset is more than 1', function(done) { 35 | var interp = L.GeometryUtil.interpolateOnLine(map, [llA, llB, llC], 10); 36 | assert.latLngEqual(interp.latLng, llC); 37 | assert.equal(interp.predecessor, 1); 38 | done(); 39 | }); 40 | 41 | it('It should not fail if line has no length', function(done) { 42 | var interp = L.GeometryUtil.interpolateOnLine(map, [llA, llA, llA], 0.5); 43 | assert.latLngEqual(interp.latLng, llA); 44 | done(); 45 | }); 46 | 47 | it('It should return the correct interpolations', function(done) { 48 | var interp1 = L.GeometryUtil.interpolateOnLine(map, [llA, llB, llC], 0.5); 49 | assert.latLngEqual(interp1.latLng, llB); 50 | var interp2 = L.GeometryUtil.interpolateOnLine(map, [llA, llB, llC], 0.75); 51 | assert.latLngEqual(interp2.latLng, L.latLng([4, 5])); 52 | done(); 53 | }); 54 | 55 | it('It should work the same with instances of L.PolyLine and arrays of L.LatLng', function(done) { 56 | var lls = [llA, llB, llC]; 57 | var withArray = L.GeometryUtil.interpolateOnLine(map, lls, 0.75); 58 | var withPolyLine = L.GeometryUtil.interpolateOnLine(map, L.polyline(lls), 0.75); 59 | assert.deepEqual(withArray, withPolyLine); 60 | done(); 61 | }); 62 | 63 | it('Should always return a LatLng object.', function() { 64 | var interp1 = L.GeometryUtil.interpolateOnLine(map, [llA, llB, llC], 0); 65 | var interp2 = L.GeometryUtil.interpolateOnLine(map, [llA, llB, llC], 1); 66 | 67 | assert.isDefined(interp1.latLng.lat); 68 | assert.isDefined(interp1.latLng.lng); 69 | assert.isDefined(interp2.latLng.lat); 70 | assert.isDefined(interp2.latLng.lng); 71 | }); 72 | 73 | it('It should not return a negative predecessor index when interpolating a point on the first segment', function() { 74 | var interp = L.GeometryUtil.interpolateOnLine(map, [llA, llB], 0.5); 75 | assert.isTrue(interp.predecessor >= 0); 76 | }); 77 | }); -------------------------------------------------------------------------------- /spec/test.interpolateOnPointSegment.js: -------------------------------------------------------------------------------- 1 | describe('Interpolate on point segment', function() { 2 | var p1 = L.point(0, 2), 3 | p2 = L.point(0, 6); 4 | it('It should be the first point if offset is 0', function(done) { 5 | assert.pointEqual(p1, L.GeometryUtil.interpolateOnPointSegment(p1, p2, 0)); 6 | done(); 7 | }); 8 | 9 | it('It should be the last point if offset is 1', function(done) { 10 | assert.pointEqual(p2, L.GeometryUtil.interpolateOnPointSegment(p1, p2, 1)); 11 | done(); 12 | }); 13 | 14 | it('It should return the correct interpolations', function(done) { 15 | assert.pointEqual(L.point(0, 4), L.GeometryUtil.interpolateOnPointSegment(p1, p2, 0.5)); 16 | assert.pointEqual(L.point(0, 5), L.GeometryUtil.interpolateOnPointSegment(p1, p2, 0.75)); 17 | done(); 18 | }); 19 | }); -------------------------------------------------------------------------------- /spec/test.isAfter.js: -------------------------------------------------------------------------------- 1 | describe('Line order', function() { 2 | var lineA = L.polyline([[0, 0], [1, 1]]), 3 | lineB = L.polyline([[1, 1], [2, 2]]); 4 | 5 | it('It should detect if line is after', function(done) { 6 | assert.isTrue(L.GeometryUtil.isAfter(lineB, lineA)); 7 | assert.isFalse(L.GeometryUtil.isAfter(lineA, lineB)); 8 | done(); 9 | }); 10 | 11 | }); -------------------------------------------------------------------------------- /spec/test.isBefore.js: -------------------------------------------------------------------------------- 1 | describe('Line order', function() { 2 | var lineA = L.polyline([[0, 0], [1, 1]]), 3 | lineB = L.polyline([[1, 1], [2, 2]]); 4 | 5 | it('It should detect if line is before', function(done) { 6 | assert.isTrue(L.GeometryUtil.isBefore(lineA, lineB)); 7 | assert.isFalse(L.GeometryUtil.isBefore(lineB, lineA)); 8 | done(); 9 | }); 10 | 11 | }); -------------------------------------------------------------------------------- /spec/test.layersWithin.js: -------------------------------------------------------------------------------- 1 | describe('Layers within a radius of the given location', function() { 2 | it('It should return an empty array if the list is empty', function(done) { 3 | var ll = L.latLng([0, 0]); 4 | var results = L.GeometryUtil.layersWithin(map, [], ll); 5 | assert.equal(0, results.length); 6 | done(); 7 | }); 8 | 9 | it('It should return an array containing one layer', function(done) { 10 | var ll = L.latLng([0, 0]); 11 | var layers = [L.marker([2, 2]), L.marker([100, 100])]; 12 | var results = L.GeometryUtil.layersWithin(map, layers, ll, 5); 13 | assert.equal(1, results.length); 14 | assert.deepEqual(results[0], {layer: layers[0], latlng: layers[0].getLatLng(), distance: Math.sqrt(2)}); 15 | done(); 16 | }); 17 | 18 | it('It should return an array containing two layers ordered by distance', function(done) { 19 | var ll = L.latLng([0, 0]); 20 | var layers = [L.marker([2, 2]), L.marker([3, 3])]; 21 | var results = L.GeometryUtil.layersWithin(map, layers, ll, 10); 22 | assert.equal(2, results.length); 23 | assert.equal(true, results[0].distance < results[1].distance); 24 | done(); 25 | }); 26 | }); -------------------------------------------------------------------------------- /spec/test.length.js: -------------------------------------------------------------------------------- 1 | describe('Length of line', function() { 2 | it('It should be 0 for empty line', function(done) { 3 | assert.equal(0, L.GeometryUtil.length([])); 4 | done(); 5 | }); 6 | 7 | it('It should return length in meters', function(done) { 8 | assert.closeTo(111319.49079327357, L.GeometryUtil.length(L.polyline([[0, 0], [1, 0]])), 500); // compatibility of Leaflet 1.0, due to earth R changed 9 | done(); 10 | }); 11 | }); -------------------------------------------------------------------------------- /spec/test.locateOnLine.js: -------------------------------------------------------------------------------- 1 | describe('Locate on line', function() { 2 | var line = L.polyline([[0,0], [1, 1], [2, 2]]); 3 | var line_narrow_angle = L.polyline([[0,0], [0, 40], [9, 0]]) 4 | 5 | it('It should return 0 if start', function(done) { 6 | assert.equal(0, L.GeometryUtil.locateOnLine(map, line, L.latLng([0, 0]))); 7 | done(); 8 | }); 9 | 10 | it('It should return 1 if end', function(done) { 11 | assert.equal(1, L.GeometryUtil.locateOnLine(map, line, L.latLng([2, 2]))); 12 | done(); 13 | }); 14 | 15 | it('It should return ratio of point', function(done) { 16 | assert.almostEqual(0.5, L.GeometryUtil.locateOnLine(map, line, L.latLng([1, 1])), 4); 17 | assert.almostEqual(0.25, L.GeometryUtil.locateOnLine(map, line, L.latLng([0.5, 0.5])), 4); 18 | assert.almostEqual(0.85, L.GeometryUtil.locateOnLine(map, line, L.latLng([1.7, 1.7])), 4); 19 | done(); 20 | }); 21 | 22 | it('It should return ratio of point', function(done) { 23 | assert.almostEqual(0.433, L.GeometryUtil.locateOnLine(map, line_narrow_angle, L.latLng([0, 35])), 4); 24 | assert.almostEqual(0.559, L.GeometryUtil.locateOnLine(map, line_narrow_angle, L.latLng([1.5, 35])), 3); 25 | done(); 26 | }); 27 | }); -------------------------------------------------------------------------------- /spec/test.nClosestLayers.js: -------------------------------------------------------------------------------- 1 | describe('N closest layers among an arry of layers', function() { 2 | it('It should return null if list is empty', function(done) { 3 | var ll = L.latLng([0, 0]), 4 | closests = L.GeometryUtil.nClosestLayers(map, [], ll, 1); 5 | assert.equal(null, closests); 6 | done(); 7 | }); 8 | 9 | it('It should return null if the input n is zero', function(done) { 10 | var ll = L.latLng([0, 0]), 11 | layers = [L.marker([2, 2])], 12 | closests = L.GeometryUtil.nClosestLayers(map, layers, ll, 0); 13 | assert.equal(null, closests); 14 | done(); 15 | }); 16 | 17 | it('It should return null if the input n is negative', function(done) { 18 | var ll = L.latLng([0, 0]), 19 | layers = [L.marker([2, 2])], 20 | closests = L.GeometryUtil.nClosestLayers(map, layers, ll, -1); 21 | assert.equal(null, closests); 22 | done(); 23 | }); 24 | 25 | it('It should return an array of objectcs with layer, latlng and distance', function(done) { 26 | var ll = L.latLng([0, 0]), 27 | layers = [L.marker([2, 2])], 28 | closests = L.GeometryUtil.nClosestLayers(map, layers, ll); 29 | assert.deepEqual(closests[0], 30 | {layer: layers[0], latlng: layers[0].getLatLng(), distance: Math.sqrt(2)}); 31 | done(); 32 | }); 33 | 34 | it('It should return an array of objects with size n and ordered with their distance', function(done) { 35 | var ll = L.latLng([0, 0]), 36 | layers = [L.marker([3, 3]), L.marker([4, 4]), L.marker([2, 2])], 37 | n = 2, 38 | closests = L.GeometryUtil.nClosestLayers(map, layers, ll, n); 39 | 40 | assert.equal(closests.length, n); 41 | assert.equal(closests[1].distance >= closests[0].distance, true); 42 | done(); 43 | }); 44 | 45 | it('It should return an array of objects with the same size of input array if n is larger than the size of input array.', function(done) { 46 | var ll = L.latLng([0, 0]), 47 | layers = [L.marker([3, 3]), L.marker([4, 4]), L.marker([2, 2])], 48 | n = 5, 49 | closests = L.GeometryUtil.nClosestLayers(map, layers, ll, n); 50 | 51 | assert.equal(closests.length, layers.length); 52 | assert.equal(closests[1].distance >= closests[0].distance, true); 53 | assert.equal(closests[2].distance >= closests[1].distance, true); 54 | done(); 55 | }); 56 | 57 | it('It should return an array of objects with the same size of input array if n is not given.', function(done) { 58 | var ll = L.latLng([0, 0]), 59 | layers = [L.marker([3, 3]), L.marker([4, 4]), L.marker([2, 2])], 60 | closests = L.GeometryUtil.nClosestLayers(map, layers, ll); 61 | 62 | assert.equal(closests.length, layers.length); 63 | done(); 64 | }); 65 | 66 | it('It should return the sub layer closest to the latlng point', function(done) { 67 | var ll = L.latLng([0,5]), 68 | campus = { 69 | "type": "Feature", 70 | "properties": { 71 | "popupContent": "This is the Auraria West Campus", 72 | "style": { 73 | weight: 2, 74 | color: "#999", 75 | opacity: 1, 76 | fillColor: "#B0DE5C", 77 | fillOpacity: 0.8 78 | } 79 | }, 80 | "geometry": { 81 | "type": "MultiPolygon", 82 | "coordinates": [ 83 | [ 84 | [ 85 | [0, 0], 86 | [0, 10], 87 | [-105.00921249389647, 39.74468219277038], 88 | [-105.01067161560059, 39.74362625960105], 89 | [-105.01195907592773, 39.74290029616054], 90 | [-105.00989913940431, 39.74078835902781], 91 | [-105.00758171081543, 39.74059036160317], 92 | [-105.00346183776855, 39.74059036160317], 93 | [-105.00097274780272, 39.74059036160317], 94 | [-105.00062942504881, 39.74072235994946], 95 | [-105.00020027160645, 39.74191033368865], 96 | [-105.00071525573731, 39.74276830198601], 97 | [-105.00097274780272, 39.74369225589818], 98 | [-105.00097274780272, 39.74461619742136], 99 | [-105.00123023986816, 39.74534214278395], 100 | [-105.00183105468751, 39.74613407445653], 101 | [-105.00432014465332, 39.74732195489861] 102 | ],[ 103 | [-105.00361204147337, 39.74354376414072], 104 | [-105.00301122665405, 39.74278480127163], 105 | [-105.00221729278564, 39.74316428375108], 106 | [-105.00283956527711, 39.74390674342741], 107 | [-105.00361204147337, 39.74354376414072] 108 | ] 109 | ],[ 110 | [ 111 | [-105.00942707061768, 39.73989736613708], 112 | [-105.00942707061768, 39.73910536278566], 113 | [-105.00685214996338, 39.73923736397631], 114 | [-105.00384807586671, 39.73910536278566], 115 | [-105.00174522399902, 39.73903936209552], 116 | [-105.00041484832764, 39.73910536278566], 117 | [-105.00041484832764, 39.73979836621592], 118 | [-105.00535011291504, 39.73986436617916], 119 | [-105.00942707061768, 39.73989736613708] 120 | ] 121 | ] 122 | ] 123 | } 124 | }, 125 | layers = L.geoJson(campus, { 126 | 127 | pointToLayer: function (feature, latlng) { 128 | return L.circleMarker(latlng, { 129 | radius: 8, 130 | fillColor: "#ff7800", 131 | color: "#000", 132 | weight: 1, 133 | opacity: 1, 134 | fillOpacity: 0.8 135 | }); 136 | } 137 | }), 138 | closests = L.GeometryUtil.nClosestLayers(map, layers.getLayers(), ll, 1); 139 | // we test instanceof because of differences between Leaflet 0.7.7 & 1.0.0 140 | // in 0.7.7 geojson are LayerGroup, in 1.0, we have directly instances of L.Polyline & co 141 | assert.deepEqual(closests[0].layer, layers.getLayers()[0] instanceof L.LayerGroup ? layers.getLayers()[0].getLayers()[0] : layers.getLayers()[0]) 142 | assert.deepEqual(closests[0].distance, 4) 143 | done(); 144 | }) 145 | }); 146 | -------------------------------------------------------------------------------- /spec/test.readableDistance.js: -------------------------------------------------------------------------------- 1 | describe('Readable distances', function() { 2 | it('It should be meters by default', function(done) { 3 | assert.equal("0.0 m", L.GeometryUtil.readableDistance(0)); 4 | done(); 5 | }); 6 | 7 | it('It should be 0.0 yd if imperial', function(done) { 8 | assert.equal("0.0 yd", L.GeometryUtil.readableDistance(0, 'imperial')); 9 | done(); 10 | }); 11 | 12 | it('It should be kilometers if superior to 1000', function(done) { 13 | assert.equal("1.01 km", L.GeometryUtil.readableDistance(1010)); 14 | done(); 15 | }); 16 | 17 | it('It should be miles if superior to 1760', function(done) { 18 | assert.equal("1.24 miles", L.GeometryUtil.readableDistance(2000, 'imperial')); 19 | done(); 20 | }); 21 | }); -------------------------------------------------------------------------------- /spec/test.reverse.js: -------------------------------------------------------------------------------- 1 | describe('Reverse line', function() { 2 | var line = L.polyline([[0,0], [1, 1]]); 3 | 4 | it('It should invert coordinates', function(done) { 5 | assert.latLngEqual(line.getLatLngs()[0], L.GeometryUtil.reverse(line).getLatLngs()[1]); 6 | done(); 7 | }); 8 | 9 | it('It should not affect original', function(done) { 10 | var start = line.getLatLngs()[0]; 11 | L.GeometryUtil.reverse(line); 12 | assert.latLngEqual(start, line.getLatLngs()[0]); 13 | done(); 14 | }); 15 | }); -------------------------------------------------------------------------------- /spec/test.rotatePoint.js: -------------------------------------------------------------------------------- 1 | describe('Point rotation', function() { 2 | it('It should return the same point if angle is 0', function(done) { 3 | var llPoint = L.latLng([3, 3]), 4 | llCenter = L.latLng([2, 2]), 5 | rotated = L.GeometryUtil.rotatePoint(map, llPoint, 0, llCenter); 6 | assert.latLngEqual(llPoint, rotated); 7 | done(); 8 | }); 9 | 10 | it('It should return the same point if center and point are the same', function(done) { 11 | var llPoint = L.latLng([1, 1]), 12 | llCenter = L.latLng([1, 1]), 13 | rotated = L.GeometryUtil.rotatePoint(map, llPoint, 90, llCenter); 14 | assert.latLngEqual(llPoint, rotated); 15 | done(); 16 | }); 17 | 18 | it('It should return a rotated point', function(done) { 19 | var llPoint = L.latLng([1, 1]), 20 | llCenter = L.latLng([2, 2]), 21 | rotated = L.GeometryUtil.rotatePoint(map, llPoint, 90, llCenter); 22 | assert.latLngEqual(rotated, L.latLng([3, 1])); 23 | done(); 24 | }); 25 | }); -------------------------------------------------------------------------------- /spec/test.startsAtExtremity.js: -------------------------------------------------------------------------------- 1 | describe('Line order', function() { 2 | var lineA = L.polyline([[0, 0], [1, 1]]), 3 | lineB = L.polyline([[1, 1], [2, 2]]); 4 | 5 | it('It should detect if line starts at extremity', function(done) { 6 | var lineC = L.polyline([[0, 0], [1, 1]]); 7 | assert.isTrue(L.GeometryUtil.startsAtExtremity(lineA, lineC)); 8 | assert.isTrue(L.GeometryUtil.startsAtExtremity(lineB, lineC)); 9 | assert.isFalse(L.GeometryUtil.startsAtExtremity(lineC, lineB)); 10 | done(); 11 | }); 12 | }); -------------------------------------------------------------------------------- /src/leaflet.geometryutil.d.ts: -------------------------------------------------------------------------------- 1 | import * as L from "leaflet"; 2 | import { LatLngLiteral, Layer } from "leaflet" 3 | 4 | interface LayerPointRelation { 5 | layer: LayerType; 6 | latlng: LatLngLiteral; 7 | distance: number; 8 | } 9 | 10 | interface LatLngWithDistance extends LatLngLiteral { 11 | distance: number; 12 | } 13 | 14 | declare module "leaflet" { 15 | namespace Polyline { 16 | function _flat(latlngs: LatLngExpression[]): boolean; 17 | } 18 | 19 | namespace GeometryUtil { 20 | 21 | function distance(map: Map, latlngA: LatLngExpression, latlngB: LatLngExpression): number; 22 | 23 | function distanceSegment(map: Map, latlng: LatLngExpression, latlngA: LatLngExpression, latlngB: LatLngExpression): number; 24 | 25 | function readableDistance(distance: number, unit?: 'metric' | 'imperial'): string; 26 | 27 | function belongsSegment(latlng: LatLngExpression, latlngA: LatLngExpression, latlngB: LatLngExpression, tolerance?: number): boolean; 28 | 29 | function length(coords: LatLng[] | Point[] | Polyline): number; 30 | 31 | function accumulatedLengths(coords: LatLng[] | Point[] | Polyline): number[]; 32 | 33 | function closestOnSegment(map: Map, latlng: LatLngExpression, latlngA: LatLngExpression, latlngB: LatLngExpression): LatLng; 34 | 35 | function closest(map: Map, layer: LatLngExpression[] | LatLngExpression[][] | Polyline | Polygon, latlng: LatLngExpression, vertices?: boolean): LatLngWithDistance | null; 36 | 37 | function closestLayer(map: Map, layers: LayerType[], latlng: LatLngExpression): LayerPointRelation | null; 38 | 39 | function nClosestLayers(map: Map, layers: LayerType[], latlng: LatLngExpression, n?: number): Array> | null; 40 | 41 | function layersWithin(map: Map, layers: LayerType[], latlng: LatLngExpression, radius?: number): Array>; 42 | 43 | function closestLayerSnap(map: Map, layers: LayerType[], latlng: LatLngExpression, tolerance?: number, withVertices?: boolean): LayerPointRelation; 44 | 45 | function interpolateOnPointSegment(pA: Point, pB: Point, ratio: number): Point; 46 | 47 | function interpolateOnLine(map: Map, latLngs: LatLngExpression[] | Polyline, ratio: number): { latLng: LatLng, predecessor: number } | null; 48 | 49 | function locateOnLine(map: Map, polyline: Polyline, latlng: LatLng): number; 50 | 51 | function reverse(polyline: Polyline): Polyline; 52 | 53 | function extract(map: Map, polyline: Polyline, start: number, end: number): LatLng[]; 54 | 55 | function isBefore(polyline: Polyline, other: Polyline): boolean; 56 | 57 | function isAfter(polyline: Polyline, other: Polyline): boolean; 58 | 59 | function startsAtExtremity(polyline: Polyline, other: Polyline): boolean; 60 | 61 | function computeAngle(a: Point, b: Point): number; 62 | 63 | function computeSlope(a: Point, b: Point): { a: number, b: number }; 64 | 65 | function rotatePoint(map: Map, latlngPoint: LatLngExpression, angleDeg: number, latlngCenter: LatLngExpression): LatLng; 66 | 67 | function bearing(latlng1: LatLngLiteral | LatLng, latlng2: LatLngLiteral | LatLng): number; 68 | 69 | function destination(latlng: LatLngLiteral | LatLng, heading: number, distance: number): LatLng; 70 | 71 | function angle(map: Map, latlngA: LatLngExpression, latlngB: LatLngExpression): number; 72 | 73 | function destinationOnSegment(map: Map, latlngA: LatLngLiteral | LatLng, latlngB: LatLngLiteral | LatLng, distance: number): LatLng; 74 | 75 | } 76 | } 77 | 78 | declare const GeometryUtil: typeof L.GeometryUtil; 79 | export default GeometryUtil; 80 | -------------------------------------------------------------------------------- /src/leaflet.geometryutil.js: -------------------------------------------------------------------------------- 1 | // Packaging/modules magic dance. 2 | (function (factory) { 3 | var L; 4 | if (typeof define === 'function' && define.amd) { 5 | // AMD 6 | define(['leaflet'], factory); 7 | } else if (typeof module !== 'undefined') { 8 | // Node/CommonJS 9 | L = require('leaflet'); 10 | module.exports = factory(L); 11 | } else { 12 | // Browser globals 13 | if (typeof window.L === 'undefined') 14 | throw 'Leaflet must be loaded first'; 15 | factory(window.L); 16 | } 17 | }(function (L) { 18 | "use strict"; 19 | 20 | L.Polyline._flat = L.LineUtil.isFlat || L.Polyline._flat || function (latlngs) { 21 | // true if it's a flat array of latlngs; false if nested 22 | return !L.Util.isArray(latlngs[0]) || (typeof latlngs[0][0] !== 'object' && typeof latlngs[0][0] !== 'undefined'); 23 | }; 24 | 25 | /** 26 | * @fileOverview Leaflet Geometry utilities for distances and linear referencing. 27 | * @name L.GeometryUtil 28 | */ 29 | 30 | L.GeometryUtil = L.extend(L.GeometryUtil || {}, { 31 | 32 | /** 33 | Shortcut function for planar distance between two {L.LatLng} at current zoom. 34 | 35 | @tutorial distance-length 36 | 37 | @param {L.Map} map Leaflet map to be used for this method 38 | @param {L.LatLng} latlngA geographical point A 39 | @param {L.LatLng} latlngB geographical point B 40 | @returns {Number} planar distance 41 | */ 42 | distance: function (map, latlngA, latlngB) { 43 | return map.latLngToLayerPoint(latlngA).distanceTo(map.latLngToLayerPoint(latlngB)); 44 | }, 45 | 46 | /** 47 | Shortcut function for planar distance between a {L.LatLng} and a segment (A-B). 48 | @param {L.Map} map Leaflet map to be used for this method 49 | @param {L.LatLng} latlng - The position to search 50 | @param {L.LatLng} latlngA geographical point A of the segment 51 | @param {L.LatLng} latlngB geographical point B of the segment 52 | @returns {Number} planar distance 53 | */ 54 | distanceSegment: function (map, latlng, latlngA, latlngB) { 55 | var p = map.latLngToLayerPoint(latlng), 56 | p1 = map.latLngToLayerPoint(latlngA), 57 | p2 = map.latLngToLayerPoint(latlngB); 58 | return L.LineUtil.pointToSegmentDistance(p, p1, p2); 59 | }, 60 | 61 | /** 62 | Shortcut function for converting distance to readable distance. 63 | @param {Number} distance distance to be converted 64 | @param {String} unit 'metric' or 'imperial' 65 | @returns {String} in yard or miles 66 | */ 67 | readableDistance: function (distance, unit) { 68 | var isMetric = (unit !== 'imperial'), 69 | distanceStr; 70 | if (isMetric) { 71 | // show metres when distance is < 1km, then show km 72 | if (distance > 1000) { 73 | distanceStr = (distance / 1000).toFixed(2) + ' km'; 74 | } 75 | else { 76 | distanceStr = distance.toFixed(1) + ' m'; 77 | } 78 | } 79 | else { 80 | distance *= 1.09361; 81 | if (distance > 1760) { 82 | distanceStr = (distance / 1760).toFixed(2) + ' miles'; 83 | } 84 | else { 85 | distanceStr = distance.toFixed(1) + ' yd'; 86 | } 87 | } 88 | return distanceStr; 89 | }, 90 | 91 | /** 92 | Returns true if the latlng belongs to segment A-B 93 | @param {L.LatLng} latlng - The position to search 94 | @param {L.LatLng} latlngA geographical point A of the segment 95 | @param {L.LatLng} latlngB geographical point B of the segment 96 | @param {?Number} [tolerance=0.2] tolerance to accept if latlng belongs really 97 | @returns {boolean} 98 | */ 99 | belongsSegment: function(latlng, latlngA, latlngB, tolerance) { 100 | tolerance = tolerance === undefined ? 0.2 : tolerance; 101 | var hypotenuse = latlngA.distanceTo(latlngB), 102 | delta = latlngA.distanceTo(latlng) + latlng.distanceTo(latlngB) - hypotenuse; 103 | return delta/hypotenuse < tolerance; 104 | }, 105 | 106 | /** 107 | * Returns total length of line 108 | * @tutorial distance-length 109 | * 110 | * @param {L.Polyline|Array|Array} coords Set of coordinates 111 | * @returns {Number} Total length (pixels for Point, meters for LatLng) 112 | */ 113 | length: function (coords) { 114 | var accumulated = L.GeometryUtil.accumulatedLengths(coords); 115 | return accumulated.length > 0 ? accumulated[accumulated.length-1] : 0; 116 | }, 117 | 118 | /** 119 | * Returns a list of accumulated length along a line. 120 | * @param {L.Polyline|Array|Array} coords Set of coordinates 121 | * @returns {Array} Array of accumulated lengths (pixels for Point, meters for LatLng) 122 | */ 123 | accumulatedLengths: function (coords) { 124 | if (typeof coords.getLatLngs == 'function') { 125 | coords = coords.getLatLngs(); 126 | } 127 | if (coords.length === 0) 128 | return []; 129 | var total = 0, 130 | lengths = [0]; 131 | for (var i = 0, n = coords.length - 1; i< n; i++) { 132 | total += coords[i].distanceTo(coords[i+1]); 133 | lengths.push(total); 134 | } 135 | return lengths; 136 | }, 137 | 138 | /** 139 | Returns the closest point of a {L.LatLng} on the segment (A-B) 140 | 141 | @tutorial closest 142 | 143 | @param {L.Map} map Leaflet map to be used for this method 144 | @param {L.LatLng} latlng - The position to search 145 | @param {L.LatLng} latlngA geographical point A of the segment 146 | @param {L.LatLng} latlngB geographical point B of the segment 147 | @returns {L.LatLng} Closest geographical point 148 | */ 149 | closestOnSegment: function (map, latlng, latlngA, latlngB) { 150 | var maxzoom = map.getMaxZoom(); 151 | if (maxzoom === Infinity) 152 | maxzoom = map.getZoom(); 153 | var p = map.project(latlng, maxzoom), 154 | p1 = map.project(latlngA, maxzoom), 155 | p2 = map.project(latlngB, maxzoom), 156 | closest = L.LineUtil.closestPointOnSegment(p, p1, p2); 157 | return map.unproject(closest, maxzoom); 158 | }, 159 | 160 | /** 161 | Returns the closest point of a {L.LatLng} on a {L.Circle} 162 | 163 | @tutorial closest 164 | 165 | @param {L.LatLng} latlng - The position to search 166 | @param {L.Circle} circle - A Circle defined by a center and a radius 167 | @returns {L.LatLng} Closest geographical point on the circle circumference 168 | */ 169 | closestOnCircle: function (circle, latLng) { 170 | const center = circle.getLatLng(); 171 | const circleRadius = circle.getRadius(); 172 | const radius = typeof circleRadius === 'number' ? circleRadius : circleRadius.radius; 173 | const x = latLng.lng; 174 | const y = latLng.lat; 175 | const cx = center.lng; 176 | const cy = center.lat; 177 | // dx and dy is the vector from the circle's center to latLng 178 | const dx = x - cx; 179 | const dy = y - cy; 180 | 181 | // distance between the point and the circle's center 182 | const distance = Math.sqrt(dx * dx + dy * dy) 183 | 184 | // Calculate the closest point on the circle by adding the normalized vector to the center 185 | const tx = cx + (dx / distance) * radius; 186 | const ty = cy + (dy / distance) * radius; 187 | 188 | return new L.LatLng(ty, tx); 189 | }, 190 | 191 | 192 | /** 193 | Returns the closest latlng on layer. 194 | 195 | Accept nested arrays 196 | 197 | @tutorial closest 198 | 199 | @param {L.Map} map Leaflet map to be used for this method 200 | @param {Array|Array>|L.PolyLine|L.Polygon} layer - Layer that contains the result 201 | @param {L.LatLng} latlng - The position to search 202 | @param {?boolean} [vertices=false] - Whether to restrict to path vertices. 203 | @returns {L.LatLng} Closest geographical point or null if layer param is incorrect 204 | */ 205 | closest: function (map, layer, latlng, vertices) { 206 | 207 | var latlngs, 208 | mindist = Infinity, 209 | result = null, 210 | i, n, distance, subResult; 211 | 212 | if (layer instanceof Array) { 213 | // if layer is Array> 214 | if (layer[0] instanceof Array && typeof layer[0][0] !== 'number') { 215 | // if we have nested arrays, we calc the closest for each array 216 | // recursive 217 | for (i = 0; i < layer.length; i++) { 218 | subResult = L.GeometryUtil.closest(map, layer[i], latlng, vertices); 219 | if (subResult && subResult.distance < mindist) { 220 | mindist = subResult.distance; 221 | result = subResult; 222 | } 223 | } 224 | return result; 225 | } else if (layer[0] instanceof L.LatLng 226 | || typeof layer[0][0] === 'number' 227 | || typeof layer[0].lat === 'number') { // we could have a latlng as [x,y] with x & y numbers or {lat, lng} 228 | layer = L.polyline(layer); 229 | } else { 230 | return result; 231 | } 232 | } 233 | 234 | // if we don't have here a Polyline, that means layer is incorrect 235 | // see https://github.com/makinacorpus/Leaflet.GeometryUtil/issues/23 236 | if (! ( layer instanceof L.Polyline ) ) 237 | return result; 238 | 239 | // deep copy of latlngs 240 | latlngs = JSON.parse(JSON.stringify(layer.getLatLngs().slice(0))); 241 | 242 | // add the last segment for L.Polygon 243 | if (layer instanceof L.Polygon) { 244 | // add the last segment for each child that is a nested array 245 | var addLastSegment = function(latlngs) { 246 | if (L.Polyline._flat(latlngs)) { 247 | latlngs.push(latlngs[0]); 248 | } else { 249 | for (var i = 0; i < latlngs.length; i++) { 250 | addLastSegment(latlngs[i]); 251 | } 252 | } 253 | }; 254 | addLastSegment(latlngs); 255 | } 256 | 257 | // we have a multi polygon / multi polyline / polygon with holes 258 | // use recursive to explore and return the good result 259 | if ( ! L.Polyline._flat(latlngs) ) { 260 | for (i = 0; i < latlngs.length; i++) { 261 | // if we are at the lower level, and if we have a L.Polygon, we add the last segment 262 | subResult = L.GeometryUtil.closest(map, latlngs[i], latlng, vertices); 263 | if (subResult.distance < mindist) { 264 | mindist = subResult.distance; 265 | result = subResult; 266 | } 267 | } 268 | return result; 269 | 270 | } else { 271 | 272 | // Lookup vertices 273 | if (vertices) { 274 | for(i = 0, n = latlngs.length; i < n; i++) { 275 | var ll = latlngs[i]; 276 | distance = L.GeometryUtil.distance(map, latlng, ll); 277 | if (distance < mindist) { 278 | mindist = distance; 279 | result = ll; 280 | result.distance = distance; 281 | } 282 | } 283 | return result; 284 | } 285 | 286 | // Keep the closest point of all segments 287 | for (i = 0, n = latlngs.length; i < n-1; i++) { 288 | var latlngA = latlngs[i], 289 | latlngB = latlngs[i+1]; 290 | distance = L.GeometryUtil.distanceSegment(map, latlng, latlngA, latlngB); 291 | if (distance <= mindist) { 292 | mindist = distance; 293 | result = L.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB); 294 | result.distance = distance; 295 | } 296 | } 297 | return result; 298 | } 299 | 300 | }, 301 | 302 | /** 303 | Returns the closest layer to latlng among a list of layers. 304 | 305 | @tutorial closest 306 | 307 | @param {L.Map} map Leaflet map to be used for this method 308 | @param {Array} layers Set of layers 309 | @param {L.LatLng} latlng - The position to search 310 | @returns {object} ``{layer, latlng, distance}`` or ``null`` if list is empty; 311 | */ 312 | closestLayer: function (map, layers, latlng) { 313 | var mindist = Infinity, 314 | result = null, 315 | ll = null, 316 | distance = Infinity; 317 | 318 | for (var i = 0, n = layers.length; i < n; i++) { 319 | var layer = layers[i]; 320 | if (layer instanceof L.LayerGroup) { 321 | // recursive 322 | var subResult = L.GeometryUtil.closestLayer(map, layer.getLayers(), latlng); 323 | if (subResult.distance < mindist) { 324 | mindist = subResult.distance; 325 | result = subResult; 326 | } 327 | } else { 328 | if (layer instanceof L.Circle){ 329 | ll = this.closestOnCircle(layer, latlng); 330 | distance = L.GeometryUtil.distance(map, latlng, ll); 331 | } else 332 | // Single dimension, snap on points, else snap on closest 333 | if (typeof layer.getLatLng == 'function') { 334 | ll = layer.getLatLng(); 335 | distance = L.GeometryUtil.distance(map, latlng, ll); 336 | } 337 | else { 338 | ll = L.GeometryUtil.closest(map, layer, latlng); 339 | if (ll) distance = ll.distance; // Can return null if layer has no points. 340 | } 341 | if (distance < mindist) { 342 | mindist = distance; 343 | result = {layer: layer, latlng: ll, distance: distance}; 344 | } 345 | } 346 | } 347 | return result; 348 | }, 349 | 350 | /** 351 | Returns the n closest layers to latlng among a list of input layers. 352 | 353 | @param {L.Map} map - Leaflet map to be used for this method 354 | @param {Array} layers - Set of layers 355 | @param {L.LatLng} latlng - The position to search 356 | @param {?Number} [n=layers.length] - the expected number of output layers. 357 | @returns {Array} an array of objects ``{layer, latlng, distance}`` or ``null`` if the input is invalid (empty list or negative n) 358 | */ 359 | nClosestLayers: function (map, layers, latlng, n) { 360 | n = typeof n === 'number' ? n : layers.length; 361 | 362 | if (n < 1 || layers.length < 1) { 363 | return null; 364 | } 365 | 366 | var results = []; 367 | var distance, ll; 368 | 369 | for (var i = 0, m = layers.length; i < m; i++) { 370 | var layer = layers[i]; 371 | if (layer instanceof L.LayerGroup) { 372 | // recursive 373 | var subResult = L.GeometryUtil.closestLayer(map, layer.getLayers(), latlng); 374 | results.push(subResult); 375 | } else { 376 | if (layer instanceof L.Circle){ 377 | ll = this.closestOnCircle(layer, latlng); 378 | distance = L.GeometryUtil.distance(map, latlng, ll); 379 | } else 380 | // Single dimension, snap on points, else snap on closest 381 | if (typeof layer.getLatLng == 'function') { 382 | ll = layer.getLatLng(); 383 | distance = L.GeometryUtil.distance(map, latlng, ll); 384 | } 385 | else { 386 | ll = L.GeometryUtil.closest(map, layer, latlng); 387 | if (ll) distance = ll.distance; // Can return null if layer has no points. 388 | } 389 | results.push({layer: layer, latlng: ll, distance: distance}); 390 | } 391 | } 392 | 393 | results.sort(function(a, b) { 394 | return a.distance - b.distance; 395 | }); 396 | 397 | if (results.length > n) { 398 | return results.slice(0, n); 399 | } else { 400 | return results; 401 | } 402 | }, 403 | 404 | /** 405 | * Returns all layers within a radius of the given position, in an ascending order of distance. 406 | @param {L.Map} map Leaflet map to be used for this method 407 | @param {Array} layers - A list of layers. 408 | @param {L.LatLng} latlng - The position to search 409 | @param {?Number} [radius=Infinity] - Search radius in pixels 410 | @return {object[]} an array of objects including layer within the radius, closest latlng, and distance 411 | */ 412 | layersWithin: function(map, layers, latlng, radius) { 413 | radius = typeof radius == 'number' ? radius : Infinity; 414 | 415 | var results = []; 416 | var ll = null; 417 | var distance = 0; 418 | 419 | for (var i = 0, n = layers.length; i < n; i++) { 420 | var layer = layers[i]; 421 | 422 | if (typeof layer.getLatLng == 'function') { 423 | ll = layer.getLatLng(); 424 | distance = L.GeometryUtil.distance(map, latlng, ll); 425 | } 426 | else { 427 | ll = L.GeometryUtil.closest(map, layer, latlng); 428 | if (ll) distance = ll.distance; // Can return null if layer has no points. 429 | } 430 | 431 | if (ll && distance < radius) { 432 | results.push({layer: layer, latlng: ll, distance: distance}); 433 | } 434 | } 435 | 436 | var sortedResults = results.sort(function(a, b) { 437 | return a.distance - b.distance; 438 | }); 439 | 440 | return sortedResults; 441 | }, 442 | 443 | /** 444 | Returns the closest position from specified {LatLng} among specified layers, 445 | with a maximum tolerance in pixels, providing snapping behaviour. 446 | 447 | @tutorial closest 448 | 449 | @param {L.Map} map Leaflet map to be used for this method 450 | @param {Array} layers - A list of layers to snap on. 451 | @param {L.LatLng} latlng - The position to snap 452 | @param {?Number} [tolerance=Infinity] - Maximum number of pixels. 453 | @param {?boolean} [withVertices=true] - Snap to layers vertices or segment points (not only vertex) 454 | @returns {object} with snapped {LatLng} and snapped {Layer} or null if tolerance exceeded. 455 | */ 456 | closestLayerSnap: function (map, layers, latlng, tolerance, withVertices) { 457 | tolerance = typeof tolerance == 'number' ? tolerance : Infinity; 458 | withVertices = typeof withVertices == 'boolean' ? withVertices : true; 459 | 460 | var result = L.GeometryUtil.closestLayer(map, layers, latlng); 461 | if (!result || result.distance > tolerance) 462 | return null; 463 | 464 | // If snapped layer is linear, try to snap on vertices (extremities and middle points) 465 | if (withVertices && typeof result.layer.getLatLngs == 'function') { 466 | var closest = L.GeometryUtil.closest(map, result.layer, result.latlng, true); 467 | if (closest.distance < tolerance) { 468 | result.latlng = closest; 469 | result.distance = L.GeometryUtil.distance(map, closest, latlng); 470 | } 471 | } 472 | return result; 473 | }, 474 | 475 | /** 476 | Returns the Point located on a segment at the specified ratio of the segment length. 477 | @param {L.Point} pA coordinates of point A 478 | @param {L.Point} pB coordinates of point B 479 | @param {Number} the length ratio, expressed as a decimal between 0 and 1, inclusive. 480 | @returns {L.Point} the interpolated point. 481 | */ 482 | interpolateOnPointSegment: function (pA, pB, ratio) { 483 | return L.point( 484 | (pA.x * (1 - ratio)) + (ratio * pB.x), 485 | (pA.y * (1 - ratio)) + (ratio * pB.y) 486 | ); 487 | }, 488 | 489 | /** 490 | Returns the coordinate of the point located on a line at the specified ratio of the line length. 491 | @param {L.Map} map Leaflet map to be used for this method 492 | @param {Array|L.PolyLine} latlngs Set of geographical points 493 | @param {Number} ratio the length ratio, expressed as a decimal between 0 and 1, inclusive 494 | @returns {Object} an object with latLng ({LatLng}) and predecessor ({Number}), the index of the preceding vertex in the Polyline 495 | (-1 if the interpolated point is the first vertex) 496 | */ 497 | interpolateOnLine: function (map, latLngs, ratio) { 498 | latLngs = (latLngs instanceof L.Polyline) ? latLngs.getLatLngs() : latLngs; 499 | var n = latLngs.length; 500 | if (n < 2) { 501 | return null; 502 | } 503 | 504 | // ensure the ratio is between 0 and 1; 505 | ratio = Math.max(Math.min(ratio, 1), 0); 506 | 507 | if (ratio === 0) { 508 | return { 509 | latLng: latLngs[0] instanceof L.LatLng ? latLngs[0] : L.latLng(latLngs[0]), 510 | predecessor: -1 511 | }; 512 | } 513 | if (ratio == 1) { 514 | return { 515 | latLng: latLngs[latLngs.length -1] instanceof L.LatLng ? latLngs[latLngs.length -1] : L.latLng(latLngs[latLngs.length -1]), 516 | predecessor: latLngs.length - 2 517 | }; 518 | } 519 | 520 | // project the LatLngs as Points, 521 | // and compute total planar length of the line at max precision 522 | var maxzoom = map.getMaxZoom(); 523 | if (maxzoom === Infinity) 524 | maxzoom = map.getZoom(); 525 | var pts = []; 526 | var lineLength = 0; 527 | for(var i = 0; i < n; i++) { 528 | pts[i] = map.project(latLngs[i], maxzoom); 529 | if(i > 0) 530 | lineLength += pts[i-1].distanceTo(pts[i]); 531 | } 532 | 533 | var ratioDist = lineLength * ratio; 534 | 535 | // follow the line segments [ab], adding lengths, 536 | // until we find the segment where the points should lie on 537 | var cumulativeDistanceToA = 0, cumulativeDistanceToB = 0; 538 | for (var i = 0; cumulativeDistanceToB < ratioDist; i++) { 539 | var pointA = pts[i], pointB = pts[i+1]; 540 | 541 | cumulativeDistanceToA = cumulativeDistanceToB; 542 | cumulativeDistanceToB += pointA.distanceTo(pointB); 543 | } 544 | 545 | if (pointA == undefined && pointB == undefined) { // Happens when line has no length 546 | var pointA = pts[0], pointB = pts[1], i = 1; 547 | } 548 | 549 | // compute the ratio relative to the segment [ab] 550 | var segmentRatio = ((cumulativeDistanceToB - cumulativeDistanceToA) !== 0) ? ((ratioDist - cumulativeDistanceToA) / (cumulativeDistanceToB - cumulativeDistanceToA)) : 0; 551 | var interpolatedPoint = L.GeometryUtil.interpolateOnPointSegment(pointA, pointB, segmentRatio); 552 | return { 553 | latLng: map.unproject(interpolatedPoint, maxzoom), 554 | predecessor: i-1 555 | }; 556 | }, 557 | 558 | /** 559 | Returns a float between 0 and 1 representing the location of the 560 | closest point on polyline to the given latlng, as a fraction of total line length. 561 | (opposite of L.GeometryUtil.interpolateOnLine()) 562 | @param {L.Map} map Leaflet map to be used for this method 563 | @param {L.PolyLine} polyline Polyline on which the latlng will be search 564 | @param {L.LatLng} latlng The position to search 565 | @returns {Number} Float between 0 and 1 566 | */ 567 | locateOnLine: function (map, polyline, latlng) { 568 | var latlngs = polyline.getLatLngs(); 569 | if (latlng.equals(latlngs[0])) 570 | return 0.0; 571 | if (latlng.equals(latlngs[latlngs.length-1])) 572 | return 1.0; 573 | 574 | var point = L.GeometryUtil.closest(map, polyline, latlng, false), 575 | lengths = L.GeometryUtil.accumulatedLengths(latlngs), 576 | total_length = lengths[lengths.length-1], 577 | portion = 0, 578 | found = false; 579 | for (var i=0, n = latlngs.length-1; i < n; i++) { 580 | var l1 = latlngs[i], 581 | l2 = latlngs[i+1]; 582 | portion = lengths[i]; 583 | if (L.GeometryUtil.belongsSegment(point, l1, l2, 0.001)) { 584 | portion += l1.distanceTo(point); 585 | found = true; 586 | break; 587 | } 588 | } 589 | if (!found) { 590 | throw "Could not interpolate " + latlng.toString() + " within " + polyline.toString(); 591 | } 592 | return portion / total_length; 593 | }, 594 | 595 | /** 596 | Returns a clone with reversed coordinates. 597 | @param {L.PolyLine} polyline polyline to reverse 598 | @returns {L.PolyLine} polyline reversed 599 | */ 600 | reverse: function (polyline) { 601 | return L.polyline(polyline.getLatLngs().slice(0).reverse()); 602 | }, 603 | 604 | /** 605 | Returns a sub-part of the polyline, from start to end. 606 | If start is superior to end, returns extraction from inverted line. 607 | @param {L.Map} map Leaflet map to be used for this method 608 | @param {L.PolyLine} polyline Polyline on which will be extracted the sub-part 609 | @param {Number} start ratio, expressed as a decimal between 0 and 1, inclusive 610 | @param {Number} end ratio, expressed as a decimal between 0 and 1, inclusive 611 | @returns {Array} new polyline 612 | */ 613 | extract: function (map, polyline, start, end) { 614 | if (start > end) { 615 | return L.GeometryUtil.extract(map, L.GeometryUtil.reverse(polyline), 1.0-start, 1.0-end); 616 | } 617 | 618 | // Bound start and end to [0-1] 619 | start = Math.max(Math.min(start, 1), 0); 620 | end = Math.max(Math.min(end, 1), 0); 621 | 622 | var latlngs = polyline.getLatLngs(), 623 | startpoint = L.GeometryUtil.interpolateOnLine(map, polyline, start), 624 | endpoint = L.GeometryUtil.interpolateOnLine(map, polyline, end); 625 | // Return single point if start == end 626 | if (start == end) { 627 | var point = L.GeometryUtil.interpolateOnLine(map, polyline, end); 628 | return [point.latLng]; 629 | } 630 | // Array.slice() works indexes at 0 631 | if (startpoint.predecessor == -1) 632 | startpoint.predecessor = 0; 633 | if (endpoint.predecessor == -1) 634 | endpoint.predecessor = 0; 635 | var result = latlngs.slice(startpoint.predecessor+1, endpoint.predecessor+1); 636 | result.unshift(startpoint.latLng); 637 | result.push(endpoint.latLng); 638 | return result; 639 | }, 640 | 641 | /** 642 | Returns true if first polyline ends where other second starts. 643 | @param {L.PolyLine} polyline First polyline 644 | @param {L.PolyLine} other Second polyline 645 | @returns {bool} 646 | */ 647 | isBefore: function (polyline, other) { 648 | if (!other) return false; 649 | var lla = polyline.getLatLngs(), 650 | llb = other.getLatLngs(); 651 | return (lla[lla.length-1]).equals(llb[0]); 652 | }, 653 | 654 | /** 655 | Returns true if first polyline starts where second ends. 656 | @param {L.PolyLine} polyline First polyline 657 | @param {L.PolyLine} other Second polyline 658 | @returns {bool} 659 | */ 660 | isAfter: function (polyline, other) { 661 | if (!other) return false; 662 | var lla = polyline.getLatLngs(), 663 | llb = other.getLatLngs(); 664 | return (lla[0]).equals(llb[llb.length-1]); 665 | }, 666 | 667 | /** 668 | Returns true if first polyline starts where second ends or start. 669 | @param {L.PolyLine} polyline First polyline 670 | @param {L.PolyLine} other Second polyline 671 | @returns {bool} 672 | */ 673 | startsAtExtremity: function (polyline, other) { 674 | if (!other) return false; 675 | var lla = polyline.getLatLngs(), 676 | llb = other.getLatLngs(), 677 | start = lla[0]; 678 | return start.equals(llb[0]) || start.equals(llb[llb.length-1]); 679 | }, 680 | 681 | /** 682 | Returns horizontal angle in degres between two points. 683 | @param {L.Point} a Coordinates of point A 684 | @param {L.Point} b Coordinates of point B 685 | @returns {Number} horizontal angle 686 | */ 687 | computeAngle: function(a, b) { 688 | return (Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI); 689 | }, 690 | 691 | /** 692 | Returns slope (Ax+B) between two points. 693 | @param {L.Point} a Coordinates of point A 694 | @param {L.Point} b Coordinates of point B 695 | @returns {Object} with ``a`` and ``b`` properties. 696 | */ 697 | computeSlope: function(a, b) { 698 | var s = (b.y - a.y) / (b.x - a.x), 699 | o = a.y - (s * a.x); 700 | return {'a': s, 'b': o}; 701 | }, 702 | 703 | /** 704 | Returns LatLng of rotated point around specified LatLng center. 705 | @param {L.LatLng} latlngPoint: point to rotate 706 | @param {double} angleDeg: angle to rotate in degrees 707 | @param {L.LatLng} latlngCenter: center of rotation 708 | @returns {L.LatLng} rotated point 709 | */ 710 | rotatePoint: function(map, latlngPoint, angleDeg, latlngCenter) { 711 | var maxzoom = map.getMaxZoom(); 712 | if (maxzoom === Infinity) 713 | maxzoom = map.getZoom(); 714 | var angleRad = angleDeg*Math.PI/180, 715 | pPoint = map.project(latlngPoint, maxzoom), 716 | pCenter = map.project(latlngCenter, maxzoom), 717 | x2 = Math.cos(angleRad)*(pPoint.x-pCenter.x) - Math.sin(angleRad)*(pPoint.y-pCenter.y) + pCenter.x, 718 | y2 = Math.sin(angleRad)*(pPoint.x-pCenter.x) + Math.cos(angleRad)*(pPoint.y-pCenter.y) + pCenter.y; 719 | return map.unproject(new L.Point(x2,y2), maxzoom); 720 | }, 721 | 722 | /** 723 | Returns the bearing in degrees clockwise from north (0 degrees) 724 | from the first L.LatLng to the second, at the first LatLng 725 | @param {L.LatLng} latlng1: origin point of the bearing 726 | @param {L.LatLng} latlng2: destination point of the bearing 727 | @returns {float} degrees clockwise from north. 728 | */ 729 | bearing: function(latlng1, latlng2) { 730 | var rad = Math.PI / 180, 731 | lat1 = latlng1.lat * rad, 732 | lat2 = latlng2.lat * rad, 733 | lon1 = latlng1.lng * rad, 734 | lon2 = latlng2.lng * rad, 735 | y = Math.sin(lon2 - lon1) * Math.cos(lat2), 736 | x = Math.cos(lat1) * Math.sin(lat2) - 737 | Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1); 738 | 739 | var bearing = ((Math.atan2(y, x) * 180 / Math.PI) + 360) % 360; 740 | return bearing >= 180 ? bearing-360 : bearing; 741 | }, 742 | 743 | /** 744 | Returns the point that is a distance and heading away from 745 | the given origin point. 746 | @param {L.LatLng} latlng: origin point 747 | @param {float} heading: heading in degrees, clockwise from 0 degrees north. 748 | @param {float} distance: distance in meters 749 | @returns {L.latLng} the destination point. 750 | Many thanks to Chris Veness at http://www.movable-type.co.uk/scripts/latlong.html 751 | for a great reference and examples. 752 | */ 753 | destination: function(latlng, heading, distance) { 754 | heading = (heading + 360) % 360; 755 | var rad = Math.PI / 180, 756 | radInv = 180 / Math.PI, 757 | R = L.CRS.Earth.R, // approximation of Earth's radius 758 | lon1 = latlng.lng * rad, 759 | lat1 = latlng.lat * rad, 760 | rheading = heading * rad, 761 | sinLat1 = Math.sin(lat1), 762 | cosLat1 = Math.cos(lat1), 763 | cosDistR = Math.cos(distance / R), 764 | sinDistR = Math.sin(distance / R), 765 | lat2 = Math.asin(sinLat1 * cosDistR + cosLat1 * 766 | sinDistR * Math.cos(rheading)), 767 | lon2 = lon1 + Math.atan2(Math.sin(rheading) * sinDistR * 768 | cosLat1, cosDistR - sinLat1 * Math.sin(lat2)); 769 | lon2 = lon2 * radInv; 770 | lon2 = lon2 > 180 ? lon2 - 360 : lon2 < -180 ? lon2 + 360 : lon2; 771 | return L.latLng([lat2 * radInv, lon2]); 772 | }, 773 | 774 | /** 775 | Returns the the angle of the given segment and the Equator in degrees, 776 | clockwise from 0 degrees north. 777 | @param {L.Map} map: Leaflet map to be used for this method 778 | @param {L.LatLng} latlngA: geographical point A of the segment 779 | @param {L.LatLng} latlngB: geographical point B of the segment 780 | @returns {Float} the angle in degrees. 781 | */ 782 | angle: function(map, latlngA, latlngB) { 783 | var pointA = map.latLngToContainerPoint(latlngA), 784 | pointB = map.latLngToContainerPoint(latlngB), 785 | angleDeg = Math.atan2(pointB.y - pointA.y, pointB.x - pointA.x) * 180 / Math.PI + 90; 786 | angleDeg += angleDeg < 0 ? 360 : 0; 787 | return angleDeg; 788 | }, 789 | 790 | /** 791 | Returns a point snaps on the segment and heading away from the given origin point a distance. 792 | @param {L.Map} map: Leaflet map to be used for this method 793 | @param {L.LatLng} latlngA: geographical point A of the segment 794 | @param {L.LatLng} latlngB: geographical point B of the segment 795 | @param {float} distance: distance in meters 796 | @returns {L.latLng} the destination point. 797 | */ 798 | destinationOnSegment: function(map, latlngA, latlngB, distance) { 799 | var angleDeg = L.GeometryUtil.angle(map, latlngA, latlngB), 800 | latlng = L.GeometryUtil.destination(latlngA, angleDeg, distance); 801 | return L.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB); 802 | }, 803 | }); 804 | 805 | return L.GeometryUtil; 806 | 807 | })); 808 | -------------------------------------------------------------------------------- /tutorials/closest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Demo of Leaflet.GeometryUtil distance and length 5 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /tutorials/distance-length.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Demo of Leaflet.GeometryUtil distance and length 5 | 6 | 7 | 9 | 10 | 11 | --------------------------------------------------------------------------------