├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.json ├── .stylelintrc.json ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── demo ├── index.html ├── script.js └── style.css ├── demo_esm ├── index.html ├── script.js └── style.css ├── demo_mapbox ├── index.html └── script.js ├── dist ├── L.Control.Locate.css ├── L.Control.Locate.css.map ├── L.Control.Locate.d.ts ├── L.Control.Locate.esm.js ├── L.Control.Locate.mapbox.css ├── L.Control.Locate.mapbox.css.map ├── L.Control.Locate.mapbox.min.css ├── L.Control.Locate.mapbox.min.css.map ├── L.Control.Locate.min.css ├── L.Control.Locate.min.css.map ├── L.Control.Locate.min.js ├── L.Control.Locate.min.js.map └── L.Control.Locate.umd.js ├── eslint.config.js ├── package-lock.json ├── package.json ├── screenshot.png └── src ├── L.Control.Locate.d.ts ├── L.Control.Locate.js ├── L.Control.Locate.mapbox.scss └── L.Control.Locate.scss /.gitignore: -------------------------------------------------------------------------------- 1 | .sass-cache 2 | bower_components/* 3 | node_modules/* 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .sass-cache 2 | bower_components/* 3 | bower.json 4 | demo_mapbox/* 5 | demo/* 6 | Gruntfile.js 7 | node_modules/* 8 | screenshot.png 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | dist/* 3 | node_modules/* -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 160, 3 | "trailingComma": "none" 4 | } 5 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-recommended-scss", "stylelint-config-prettier-scss"], 3 | "plugins": ["stylelint-prettier"], 4 | "root": true, 5 | "rules": { 6 | "prettier/prettier": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Here we document breaking changes. 4 | 5 | ## **0.78** 6 | 7 | - Dark mode support. 8 | 9 | ## **0.70** 10 | 11 | - Use scaling instead of setting r via CSS as it's not allowed in the SVG 1.1 specification. Thanks to @KristjanESPERANTO. 12 | 13 | ## **0.69** 14 | 15 | - Support functions for `strings.popup`. Thanks to @simon04. 16 | 17 | ## **0.64** 18 | 19 | Thanks to @brendanheywood for the updates! 20 | 21 | - Add support for heading. 22 | - Modernize style. Breathing location marker. 23 | - Use Leaflet marker. 24 | 25 | ## **0.63** 26 | 27 | - Change default `setView` from `untilPan` to `untilPanOrZoom`. 28 | 29 | ## **0.59** 30 | 31 | - Add `cacheLocation` option. 32 | 33 | ## **0.57, 0.58** 34 | 35 | - Apply marker style only to markers that support it. Fixes #169 36 | 37 | ## **0.54** 38 | 39 | - Support `flyTo` 40 | 41 | ## **0.50** 42 | 43 | - extended `setView` to support more options 44 | - removed `remainActive`, use `clickBehavior` 45 | - removed `follow`, use `setView` 46 | - removed `stopFollowingOnDrag`, use `setView` 47 | - removed `startfollowing` and `startfollowing` events 48 | - changed a few internal methods 49 | - add `drawMarker` 50 | - small fixes 51 | 52 | ## **0.46.0** 53 | 54 | - Remove IE specific CSS 55 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | var banner = "/*! Version: <%= pkg.version %>\nCopyright (c) 2016 Dominik Moritz */\n"; 3 | 4 | // Project configuration. 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON("package.json"), 7 | copy: { 8 | tsd: { 9 | src: "src/L.Control.Locate.d.ts", 10 | dest: "dist/L.Control.Locate.d.ts" 11 | } 12 | }, 13 | uglify: { 14 | options: { 15 | banner: banner, 16 | preserveComments: false, 17 | sourceMap: true 18 | }, 19 | build: { 20 | src: "dist/L.Control.Locate.umd.js", 21 | dest: "dist/L.Control.Locate.min.js" 22 | } 23 | }, 24 | sass: { 25 | options: { 26 | implementation: require("sass"), 27 | sourceMap: true 28 | }, 29 | dist: { 30 | options: { 31 | style: "compressed" 32 | }, 33 | files: { 34 | "dist/L.Control.Locate.min.css": "src/L.Control.Locate.scss", 35 | "dist/L.Control.Locate.mapbox.min.css": "src/L.Control.Locate.mapbox.scss" 36 | } 37 | }, 38 | uncompressed: { 39 | options: { 40 | style: "expanded" 41 | }, 42 | files: { 43 | "dist/L.Control.Locate.css": "src/L.Control.Locate.scss", 44 | "dist/L.Control.Locate.mapbox.css": "src/L.Control.Locate.mapbox.scss" 45 | } 46 | } 47 | }, 48 | bump: { 49 | options: { 50 | files: ["package.json", "bower.json"], 51 | commitFiles: [ 52 | "package.json", 53 | "package-lock.json", 54 | "bower.json", 55 | "dist/L.Control.Locate.css", 56 | "dist/L.Control.Locate.min.css", 57 | "dist/L.Control.Locate.min.css.map", 58 | "dist/L.Control.Locate.d.ts", 59 | "dist/L.Control.Locate.esm.js", 60 | "dist/L.Control.Locate.mapbox.css", 61 | "dist/L.Control.Locate.mapbox.min.css", 62 | "dist/L.Control.Locate.mapbox.min.css.map", 63 | "dist/L.Control.Locate.min.js", 64 | "dist/L.Control.Locate.min.js.map" 65 | ], 66 | push: false 67 | } 68 | }, 69 | connect: { 70 | server: { 71 | options: { 72 | port: 9000, 73 | protocol: "https", 74 | keepalive: true 75 | } 76 | } 77 | }, 78 | rollup: { 79 | options: { 80 | plugins: [require("@rollup/plugin-node-resolve").nodeResolve(), require("@rollup/plugin-commonjs")()] 81 | }, 82 | build_es: { 83 | options: { 84 | format: "es", 85 | external: ["leaflet"] 86 | }, 87 | files: { 88 | "dist/L.Control.Locate.esm.js": "src/L.Control.Locate.js" 89 | } 90 | }, 91 | build_umd: { 92 | options: { 93 | format: "umd", 94 | name: "L.Control.Locate", 95 | external: ["leaflet"], 96 | globals: { 97 | leaflet: "L" 98 | }, 99 | footer: ` 100 | (function() { 101 | if (typeof window !== 'undefined' && window.L) { 102 | window.L.control = window.L.control || {}; 103 | window.L.control.locate = window.L.Control.Locate.locate; 104 | } 105 | })(); 106 | ` 107 | }, 108 | files: { 109 | "dist/L.Control.Locate.umd.js": "src/L.Control.Locate.js" 110 | } 111 | } 112 | } 113 | }); 114 | 115 | grunt.loadNpmTasks("grunt-contrib-uglify"); 116 | grunt.loadNpmTasks("grunt-sass"); 117 | grunt.loadNpmTasks("grunt-rollup"); 118 | grunt.loadNpmTasks("grunt-bump"); 119 | grunt.loadNpmTasks("grunt-contrib-connect"); 120 | grunt.loadNpmTasks("grunt-contrib-copy"); 121 | 122 | // Default task(s). 123 | grunt.registerTask("default", ["rollup:build_es", "rollup:build_umd", "copy", "uglify", "sass"]); 124 | }; 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dominik Moritz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Leaflet.Locate 2 | 3 | [![npm version](https://badge.fury.io/js/leaflet.locatecontrol.svg)](http://badge.fury.io/js/leaflet.locatecontrol) 4 | [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/leaflet.locatecontrol/badge?style=rounded)](https://www.jsdelivr.com/package/npm/leaflet.locatecontrol) 5 | 6 | A useful control to geolocate the user with many options. Official [Leaflet](http://leafletjs.com/plugins.html#geolocation) and [MapBox plugin](https://www.mapbox.com/mapbox.js/example/v1.0.0/leaflet-locatecontrol/). 7 | 8 | Tested with [Leaflet](http://leafletjs.com/) 1.9.2 and [Mapbox.js](https://docs.mapbox.com/mapbox.js/) 3.3.1 in Firefox, Chrome and Safari. 9 | 10 | Please check for [breaking changes in the changelog](https://github.com/domoritz/leaflet-locatecontrol/blob/gh-pages/CHANGELOG.md). 11 | 12 | ## Demo 13 | 14 | - [Demo with Leaflet](https://domoritz.github.io/leaflet-locatecontrol/demo/) 15 | - [Demo with Mapbox.js](https://domoritz.github.io/leaflet-locatecontrol/demo_mapbox/) 16 | 17 | ## Basic Usage 18 | 19 | ### Set up: 20 | 21 | 1. Get CSS and JavaScript files 22 | 2. Include CSS and JavaScript files 23 | 3. Initialize plugin 24 | 25 | #### Download JavaScript and CSS files 26 | 27 | For testing purposes and development, you can use the latest version directly from my repository. 28 | 29 | For production environments, use [Bower](http://bower.io/) and run `bower install leaflet.locatecontrol` or [download the files from this repository](https://github.com/domoritz/leaflet-locatecontrol/archive/gh-pages.zip). Bower will always download the latest version and keep the code up to date. The original JS and CSS files are in [`\src`](https://github.com/domoritz/leaflet-locatecontrol/tree/gh-pages/src) and the minified versions suitable for production are in [`\dist`](https://github.com/domoritz/leaflet-locatecontrol/tree/gh-pages/dist). 30 | 31 | You can also get the latest version of the plugin with [npm](https://www.npmjs.org/). This plugin is available in the [npm repository](https://www.npmjs.org/package/leaflet.locatecontrol). Just run `npm install leaflet.locatecontrol`. 32 | 33 | The control is [available from JsDelivr CDN](https://www.jsdelivr.com/projects/leaflet.locatecontrol). If you don't need the latest version, you can use the [mapbox CDN](https://www.mapbox.com/mapbox.js/plugins/#leaflet-locatecontrol). 34 | 35 | #### Add the JavaScript and CSS files 36 | 37 | Then include the CSS and JavaScript files. 38 | 39 | ##### With CDN 40 | 41 | In this example, we are loading the [files from the JsDelivr CDN](https://www.jsdelivr.com/package/npm/leaflet.locatecontrol?path=dist). In the URLs below, replace `[VERSION]` with the latest release number or remove `@[VERSION]` to always use the latest version. 42 | 43 | ```html 44 | 45 | 46 | ``` 47 | 48 | ##### With `npm` 49 | 50 | ```ts 51 | import "leaflet.locatecontrol"; // Import plugin 52 | import "leaflet.locatecontrol/dist/L.Control.Locate.min.css"; // Import styles 53 | import L from "leaflet"; // Import L from leaflet to start using the plugin 54 | ``` 55 | 56 | If you are using a bundler or esm, use 57 | 58 | ```ts 59 | import { LocateControl } from "leaflet.locatecontrol"; 60 | import "leaflet.locatecontrol/dist/L.Control.Locate.min.css"; 61 | ``` 62 | 63 | Then use `new LocateControl()` instead of `L.control.locate()`. 64 | 65 | #### Add the following snippet to your map initialization: 66 | 67 | This snippet adds the control to the map. You can pass also pass a configuration. 68 | 69 | ```js 70 | L.control.locate().addTo(map); 71 | ``` 72 | 73 | ### Possible options 74 | 75 | The locate controls inherits options from [Leaflet Controls](http://leafletjs.com/reference.html#control-options). 76 | 77 | To customize the control, pass an object with your custom options to the locate control. 78 | 79 | ```js 80 | L.control.locate(OPTIONS).addTo(map); 81 | ``` 82 | 83 | Possible options are listed in the following table. More details are [in the code](https://github.com/domoritz/leaflet-locatecontrol/blob/gh-pages/src/L.Control.Locate.js#L118). 84 | 85 | 86 | | Option | Type | Description | Default | 87 | |------------|-----------|-------------------|----------| 88 | | `position` | `string` | Position of the control | `topleft` | 89 | | `layer` | [`ILayer`](http://leafletjs.com/reference.html#ilayer) | The layer that the user's location should be drawn on. | a new layer | 90 | | `setView` | `boolean` or `string` | Set the map view (zoom and pan) to the user's location as it updates. Options are `false`, `'once'`, `'always'`, `'untilPan'`, or `'untilPanOrZoom'` | `'untilPanOrZoom'` | 91 | | `flyTo` | `boolean` | Smooth pan and zoom to the location of the marker. Only works in Leaflet 1.0+. | `false` | 92 | | `keepCurrentZoomLevel` | `boolean` | Only pan when setting the view. | `false` | 93 | | `initialZoomLevel` | `false` or `integer` | After activating the plugin by clicking on the icon, zoom to the selected zoom level, even when keepCurrentZoomLevel is true. Set to `false` to disable this feature. | `false` | 94 | | `clickBehavior` | `object` | What to do when the user clicks on the control. Has three options `inView`, `inViewNotFollowing` and `outOfView`. Possible values are `stop` and `setView`, or the name of a behaviour to inherit from. | `{inView: 'stop', outOfView: 'setView', inViewNotFollowing: 'inView'}` | 95 | | `returnToPrevBounds` | `boolean` | If set, save the map bounds just before centering to the user's location. When control is disabled, set the view back to the bounds that were saved. | `false` | 96 | | `cacheLocation` | `boolean` | Keep a cache of the location after the user deactivates the control. If set to false, the user has to wait until the locate API returns a new location before they see where they are again. | `true` | 97 | | `showCompass` | `boolean` | Show the compass bearing on top of the location marker | `true` | 98 | | `drawCircle` | `boolean` | If set, a circle that shows the location accuracy is drawn. | `true` | 99 | | `drawMarker` | `boolean` | If set, the marker at the users' location is drawn. | `true` | 100 | | `markerClass` | `class` | The class to be used to create the marker. | `LocationMarker` | 101 | | `compassClass` | `class` | The class to be used to create the marker. | `CompassMarker` | 102 | | `circleStyle` | [`Path options`](http://leafletjs.com/reference.html#path-options) | Accuracy circle style properties. | see code | 103 | | `markerStyle` | [`Path options`](http://leafletjs.com/reference.html#path-options) | Inner marker style properties. Only works if your marker class supports `setStyle`. | see code | 104 | | `compassStyle` | [`Path options`](http://leafletjs.com/reference.html#path-options) | Triangle compass heading marker style properties. Only works if your marker class supports `setStyle`. | see code | 105 | | `followCircleStyle` | [`Path options`](http://leafletjs.com/reference.html#path-options) | Changes to the accuracy circle while following. Only need to provide changes. | `{}` | 106 | | `followMarkerStyle` | [`Path options`](http://leafletjs.com/reference.html#path-options) | Changes to the inner marker while following. Only need to provide changes. | `{}` | 107 | | `followCompassStyle` | [`Path options`](http://leafletjs.com/reference.html#path-options) | Changes to the compass marker while following. Only need to provide changes. | `{}` | 108 | | `icon` | `string` | The CSS class for the icon. | `leaflet-control-locate-location-arrow` | 109 | | `iconLoading` | `string` | The CSS class for the icon while loading. | `leaflet-control-locate-spinner` | 110 | | `iconElementTag` | `string` | The element to be created for icons. | `span` | 111 | | `circlePadding` | `array` | Padding around the accuracy circle. | `[0, 0]` | 112 | | `createButtonCallback` | `function` | This callback can be used in case you would like to override button creation behavior. | see code | 113 | | `getLocationBounds` | `function` | This callback can be used to override the viewport tracking behavior. | see code | 114 | | `onLocationError` | `function` | This even is called when the user's location is outside the bounds set on the map. | see code | 115 | | `metric` | `boolean` | Use metric units. | `true` | 116 | | `onLocationOutsideMapBounds` | `function` | Called when the user's location is outside the bounds set on the map. Called repeatedly when the user's location changes. | see code | 117 | | `showPopup` | `boolean` | Display a pop-up when the user click on the inner marker. | `true` | 118 | | `strings` | `object` | Strings used in the control. Options are `title`, `text`, `metersUnit`, `feetUnit`, `popup` and `outsideMapBoundsMsg` | see code | 119 | | `strings.popup` | `string` or `function` | The string shown as popup. May contain the placeholders `{distance}` and `{unit}`. If this option is specified as function, it will be executed with a single parameter `{distance, unit}` and expected to return a string. | see code | 120 | | `locateOptions` | [`Locate options`](http://leafletjs.com/reference.html#map-locate-options) | The default options passed to leaflets locate method. | see code | 121 | 122 | 123 | For example, to customize the position and the title, you could write 124 | 125 | ```js 126 | var lc = L.control 127 | .locate({ 128 | position: "topright", 129 | strings: { 130 | title: "Show me where I am, yo!" 131 | } 132 | }) 133 | .addTo(map); 134 | ``` 135 | 136 | ## Screenshot 137 | 138 | ![screenshot](https://raw.github.com/domoritz/leaflet-locatecontrol/gh-pages/screenshot.png "Screenshot showing the locate control") 139 | 140 | ## Users 141 | 142 | Sites that use this locate control: 143 | 144 | - [OpenStreetMap](http://www.openstreetmap.org/) on the start page 145 | - [MapBox](https://www.mapbox.com/mapbox.js/example/v1.0.0/leaflet-locatecontrol/) 146 | - [wheelmap.org](http://wheelmap.org/map) 147 | - [OpenMensa](http://openmensa.org/) 148 | - [Maps Marker Pro](https://www.mapsmarker.com) (WordPress plugin) 149 | - [Bikemap](https://jackdougherty.github.io/bikemapcode/) 150 | - [MyRoutes](https://myroutes.io/) 151 | - [NearbyWiki](https://en.nearbywiki.org/) 152 | - ... 153 | 154 | ## Advanced Usage 155 | 156 | ### Methods 157 | 158 | You can call `start()` or `stop()` on the locate control object to set the location on page load for example. 159 | 160 | ```js 161 | // create control and add to map 162 | var lc = L.control.locate().addTo(map); 163 | 164 | // request location update and set location 165 | lc.start(); 166 | ``` 167 | 168 | You can keep the plugin active but stop following using `lc.stopFollowing()`. 169 | 170 | ### Events 171 | 172 | You can leverage the native Leaflet events `locationfound` and `locationerror` to handle when geolocation is successful or produces an error. You can find out more about these events in the [Leaflet documentation](http://leafletjs.com/examples/mobile.html#geolocation). 173 | 174 | Additionally, the locate control fires `locateactivate` and `locatedeactivate` events on the map object when it is activated and deactivated, respectively. 175 | 176 | ### Extending 177 | 178 | To customize the behavior of the plugin, use L.extend to override `start`, `stop`, `_drawMarker` and/or `_removeMarker`. Please be aware that functions may change and customizations become incompatible. 179 | 180 | ```js 181 | L.Control.MyLocate = L.Control.Locate.extend({ 182 | _drawMarker: function () { 183 | // override to customize the marker 184 | } 185 | }); 186 | 187 | var lc = new L.Control.MyLocate(); 188 | ``` 189 | 190 | ### FAQ 191 | 192 | #### How do I set the maximum zoom level? 193 | 194 | Set the `maxZoom` in `locateOptions` (`keepCurrentZoomLevel` must not be set to true). 195 | 196 | ```js 197 | map.addControl( 198 | L.control.locate({ 199 | locateOptions: { 200 | maxZoom: 10 201 | } 202 | }) 203 | ); 204 | ``` 205 | 206 | #### How do I enable high accuracy? 207 | 208 | To enable [high accuracy (GPS) mode](http://leafletjs.com/reference.html#map-enablehighaccuracy), set the `enableHighAccuracy` in `locateOptions`. 209 | 210 | ```js 211 | map.addControl( 212 | L.control.locate({ 213 | locateOptions: { 214 | enableHighAccuracy: true 215 | } 216 | }) 217 | ); 218 | ``` 219 | 220 | #### Safari does not work with Leaflet 1.7.1 221 | 222 | This is a bug in Leaflet. Disable tap to fix it for now. See [this issue](https://github.com/Leaflet/Leaflet/issues/7255) for details. 223 | 224 | ```js 225 | let map = new L.Map('map', { 226 | tap: false, 227 | ... 228 | }); 229 | ``` 230 | 231 | ## Developers 232 | 233 | Run the demo locally with `yarn start` or `npm run start` and then open [localhost:9000/demo/index.html](http://localhost:9000/demo/index.html). 234 | 235 | To generate the minified JS and CSS files, use [grunt](http://gruntjs.com/getting-started) and run `grunt`. However, don't include new minified files or a new version as part of a pull request. If you need SASS, install it with `brew install sass/sass/sass`. 236 | 237 | ## Prettify and linting 238 | 239 | Before a Pull Request please check the code style. 240 | 241 | Run `npm run lint` to check if there are code style or linting issues. 242 | 243 | Run `npm run:fix` to automatically fix style and linting issues. 244 | 245 | ## Making a release (only core developer) 246 | 247 | A new version is released with `npm run bump:minor`. Then push the new code with `git push && git push --tags` and publish to npm with `npm publish`. 248 | 249 | ### Terms 250 | 251 | - **active**: After we called `map.locate()` and before `map.stopLocate()`. Any time, the map can fire the `locationfound` or `locationerror` events. 252 | - **following**: Following refers to whether the map zooms and pans automatically when a new location is found. 253 | 254 | ## Thanks 255 | 256 | To all [contributors](https://github.com/domoritz/leaflet-locatecontrol/contributors) and issue reporters. 257 | 258 | ## License 259 | 260 | MIT 261 | 262 | SVG icons from [Font Awesome v5.15.4](https://github.com/FortAwesome/Font-Awesome/releases/tag/5.15.4): [Creative Commons Attribution 4.0](https://fontawesome.com/license/free) 263 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet.locatecontrol", 3 | "version": "0.84.2", 4 | "homepage": "https://github.com/domoritz/leaflet-locatecontrol", 5 | "authors": ["Dominik Moritz "], 6 | "description": "A useful control to geolocate the user with many options. Used by osm.org and mapbox among many others.", 7 | "main": ["dist/L.Control.Locate.css", "src/L.Control.Locate.js"], 8 | "directories": { 9 | "example": "demo" 10 | }, 11 | "ignore": ["**/.*", "node_modules", "bower_components"], 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:domoritz/leaflet-locatecontrol.git" 15 | }, 16 | "keywords": ["leaflet", "locate", "plugin"], 17 | "license": "MIT", 18 | "readmeFilename": "README.md" 19 | } 20 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Demo Leaflet.Locate - Leaflet 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
Fork me on GitHub
16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /demo/script.js: -------------------------------------------------------------------------------- 1 | const osmUrl = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"; 2 | const osmAttrib = 'Map data © OpenStreetMap contributors'; 3 | let osm = new L.TileLayer(osmUrl, { 4 | attribution: osmAttrib, 5 | detectRetina: true 6 | }); 7 | 8 | // please replace this with your own mapbox token! 9 | const token = "pk.eyJ1IjoiZG9tb3JpdHoiLCJhIjoiY2s4a2d0OHp3MDFxMTNmcWoxdDVmdHF4MiJ9.y9-0BZCXJBpNBzEHxhFq1Q"; 10 | const mapboxUrl = "https://api.mapbox.com/styles/v1/mapbox/streets-v10/tiles/{z}/{x}/{y}@2x?access_token=" + token; 11 | const mapboxAttrib = 'Map data © OpenStreetMap contributors. Tiles from Mapbox.'; 12 | let mapbox = new L.TileLayer(mapboxUrl, { 13 | attribution: mapboxAttrib, 14 | tileSize: 512, 15 | zoomOffset: -1 16 | }); 17 | 18 | let map = new L.Map("map", { 19 | layers: [osm], 20 | center: [51.505, -0.09], 21 | zoom: 10, 22 | zoomControl: true 23 | }); 24 | 25 | // add location control to global name space for testing only 26 | // on a production site, omit the "lc = "! 27 | lc = L.control 28 | .locate({ 29 | strings: { 30 | title: "Show me where I am, yo!" 31 | } 32 | }) 33 | .addTo(map); 34 | -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | #map { 8 | position: absolute; 9 | width: 100%; 10 | height: 100%; 11 | } 12 | 13 | /* 14 | The part below is for a fancy "Fork me on GitHub" ribbon in the top right corner. 15 | It was created with the help of https://github.com/codepo8/css-fork-on-github-ribbon. 16 | */ 17 | 18 | #forkmeongithub a { 19 | background: #bb1111cc; 20 | color: #fff; 21 | font-family: arial, sans-serif; 22 | font-size: 1rem; 23 | font-weight: bold; 24 | text-align: center; 25 | text-decoration: none; 26 | text-shadow: 2px 2px #00000055; 27 | line-height: 1.4rem; 28 | padding: 5px 40px; 29 | top: -150px; 30 | } 31 | 32 | #forkmeongithub a:hover { 33 | background: #333388bb; 34 | } 35 | 36 | #forkmeongithub a::before, 37 | #forkmeongithub a::after { 38 | content: ""; 39 | width: 100%; 40 | display: block; 41 | position: absolute; 42 | top: 1px; 43 | left: 0; 44 | height: 1px; 45 | background: #fff; 46 | } 47 | 48 | #forkmeongithub a::after { 49 | bottom: 1px; 50 | top: auto; 51 | } 52 | 53 | @media screen and (min-width: 800px) { 54 | #forkmeongithub { 55 | position: fixed; 56 | display: block; 57 | top: -10px; 58 | right: -10px; 59 | width: 200px; 60 | overflow: hidden; 61 | height: 200px; 62 | z-index: 9999; 63 | } 64 | #forkmeongithub a { 65 | width: 200px; 66 | position: absolute; 67 | top: 60px; 68 | right: -60px; 69 | transform: rotate(45deg); 70 | box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.8); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /demo_esm/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet Locate Control ES Module Example 5 | 6 | 7 | 8 | 9 | 11 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /demo_esm/script.js: -------------------------------------------------------------------------------- 1 | import { Map, TileLayer } from "leaflet"; 2 | import { LocateControl } from "../dist/L.Control.Locate.esm.js"; 3 | 4 | const osmUrl = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"; 5 | const osmAttrib = 'Map data © OpenStreetMap contributors'; 6 | let osm = new TileLayer(osmUrl, { 7 | attribution: osmAttrib, 8 | detectRetina: true 9 | }); 10 | 11 | let map = new Map("map", { 12 | layers: [osm], 13 | center: [51.505, -0.09], 14 | zoom: 10, 15 | zoomControl: true 16 | }); 17 | 18 | let lc = new LocateControl({ 19 | strings: { 20 | title: "Show me where I am, yo!" 21 | } 22 | }).addTo(map); 23 | -------------------------------------------------------------------------------- /demo_esm/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | #map { 8 | position: absolute; 9 | width: 100%; 10 | height: 100%; 11 | } 12 | 13 | /* 14 | The part below is for a fancy "Fork me on GitHub" ribbon in the top right corner. 15 | It was created with the help of https://github.com/codepo8/css-fork-on-github-ribbon. 16 | */ 17 | 18 | #forkmeongithub a { 19 | background: #bb1111cc; 20 | color: #fff; 21 | font-family: arial, sans-serif; 22 | font-size: 1rem; 23 | font-weight: bold; 24 | text-align: center; 25 | text-decoration: none; 26 | text-shadow: 2px 2px #00000055; 27 | line-height: 1.4rem; 28 | padding: 5px 40px; 29 | top: -150px; 30 | } 31 | 32 | #forkmeongithub a:hover { 33 | background: #333388bb; 34 | } 35 | 36 | #forkmeongithub a::before, 37 | #forkmeongithub a::after { 38 | content: ""; 39 | width: 100%; 40 | display: block; 41 | position: absolute; 42 | top: 1px; 43 | left: 0; 44 | height: 1px; 45 | background: #fff; 46 | } 47 | 48 | #forkmeongithub a::after { 49 | bottom: 1px; 50 | top: auto; 51 | } 52 | 53 | @media screen and (min-width: 800px) { 54 | #forkmeongithub { 55 | position: fixed; 56 | display: block; 57 | top: -10px; 58 | right: -10px; 59 | width: 200px; 60 | overflow: hidden; 61 | height: 200px; 62 | z-index: 9999; 63 | } 64 | #forkmeongithub a { 65 | width: 200px; 66 | position: absolute; 67 | top: 60px; 68 | right: -60px; 69 | transform: rotate(45deg); 70 | box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.8); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /demo_mapbox/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Demo Leaflet.Locate - Mapbox.js 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
Fork me on GitHub
16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /demo_mapbox/script.js: -------------------------------------------------------------------------------- 1 | // please replace this with your own mapbox token! 2 | L.mapbox.accessToken = "pk.eyJ1IjoiZG9tb3JpdHoiLCJhIjoiY2s4a2d0OHp3MDFxMTNmcWoxdDVmdHF4MiJ9.y9-0BZCXJBpNBzEHxhFq1Q"; 3 | 4 | let map = L.mapbox.map("map").setView([51.505, -0.09], 10); 5 | L.mapbox.styleLayer("mapbox://styles/mapbox/streets-v10").addTo(map); 6 | 7 | // add location control to global name space for testing only 8 | // on a production site, omit the "lc = "! 9 | lc = L.control 10 | .locate({ 11 | strings: { 12 | title: "Show me where I am, yo!" 13 | } 14 | }) 15 | .addTo(map); 16 | -------------------------------------------------------------------------------- /dist/L.Control.Locate.css: -------------------------------------------------------------------------------- 1 | .leaflet-control-locate a { 2 | cursor: pointer; 3 | } 4 | .leaflet-control-locate a .leaflet-control-locate-location-arrow { 5 | display: inline-block; 6 | width: 16px; 7 | height: 16px; 8 | margin: 7px; 9 | background-image: url('data:image/svg+xml;charset=UTF-8,'); 10 | } 11 | .leaflet-control-locate a .leaflet-control-locate-spinner { 12 | display: inline-block; 13 | width: 16px; 14 | height: 16px; 15 | margin: 7px; 16 | background-image: url('data:image/svg+xml;charset=UTF-8,'); 17 | animation: leaflet-control-locate-spin 2s linear infinite; 18 | } 19 | .leaflet-control-locate.active a .leaflet-control-locate-location-arrow { 20 | background-image: url('data:image/svg+xml;charset=UTF-8,'); 21 | } 22 | .leaflet-control-locate.following a .leaflet-control-locate-location-arrow { 23 | background-image: url('data:image/svg+xml;charset=UTF-8,'); 24 | } 25 | 26 | .leaflet-touch .leaflet-bar .leaflet-locate-text-active { 27 | width: 100%; 28 | max-width: 200px; 29 | text-overflow: ellipsis; 30 | white-space: nowrap; 31 | overflow: hidden; 32 | padding: 0 10px; 33 | } 34 | .leaflet-touch .leaflet-bar .leaflet-locate-text-active .leaflet-locate-icon { 35 | padding: 0 5px 0 0; 36 | } 37 | 38 | .leaflet-control-locate-location circle { 39 | animation: leaflet-control-locate-throb 4s ease infinite; 40 | } 41 | 42 | @keyframes leaflet-control-locate-throb { 43 | 0% { 44 | stroke-width: 1; 45 | } 46 | 50% { 47 | stroke-width: 3; 48 | transform: scale(0.8, 0.8); 49 | } 50 | 100% { 51 | stroke-width: 1; 52 | } 53 | } 54 | @keyframes leaflet-control-locate-spin { 55 | 0% { 56 | transform: rotate(0deg); 57 | } 58 | 100% { 59 | transform: rotate(360deg); 60 | } 61 | } -------------------------------------------------------------------------------- /dist/L.Control.Locate.css.map: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domoritz/leaflet-locatecontrol/70b5bd24cdb5fa1debddbf9cd3d37f957278873e/dist/L.Control.Locate.css.map -------------------------------------------------------------------------------- /dist/L.Control.Locate.d.ts: -------------------------------------------------------------------------------- 1 | import { Control, Layer, Map, ControlOptions, PathOptions, MarkerOptions, LocationEvent, LatLngBounds, LocateOptions as LeafletLocateOptions } from "leaflet"; 2 | 3 | export type SetView = false | "once" | "always" | "untilPan" | "untilPanOrZoom"; 4 | export type ClickBehavior = "stop" | "setView"; 5 | 6 | export interface StringsOptions { 7 | title?: string | undefined; 8 | metersUnit?: string | undefined; 9 | feetUnit?: string | undefined; 10 | popup?: string | undefined; 11 | outsideMapBoundsMsg?: string | undefined; 12 | } 13 | 14 | export interface ClickBehaviorOptions { 15 | inView?: ClickBehavior | undefined; 16 | outOfView?: ClickBehavior | undefined; 17 | inViewNotFollowing?: ClickBehavior | "inView" | undefined; 18 | } 19 | 20 | export interface LocateOptions extends ControlOptions { 21 | layer?: Layer | undefined; 22 | setView?: SetView | undefined; 23 | keepCurrentZoomLevel?: boolean | undefined; 24 | initialZoomLevel?: number | boolean | undefined; 25 | getLocationBounds?: ((locationEvent: LocationEvent) => LatLngBounds) | undefined; 26 | flyTo?: boolean | undefined; 27 | clickBehavior?: ClickBehaviorOptions | undefined; 28 | returnToPrevBounds?: boolean | undefined; 29 | cacheLocation?: boolean | undefined; 30 | drawCircle?: boolean | undefined; 31 | drawMarker?: boolean | undefined; 32 | showCompass?: boolean | undefined; 33 | markerClass?: any; 34 | compassClass?: any; 35 | circleStyle?: PathOptions | undefined; 36 | markerStyle?: PathOptions | MarkerOptions | undefined; 37 | compassStyle?: PathOptions | undefined; 38 | followCircleStyle?: PathOptions | undefined; 39 | followMarkerStyle?: PathOptions | undefined; 40 | icon?: string | undefined; 41 | iconLoading?: string | undefined; 42 | iconElementTag?: string | undefined; 43 | textElementTag?: string | undefined; 44 | circlePadding?: number[] | undefined; 45 | metric?: boolean | undefined; 46 | createButtonCallback?: ((container: HTMLDivElement, options: LocateOptions) => { link: HTMLAnchorElement; icon: HTMLElement }) | undefined; 47 | onLocationError?: ((event: ErrorEvent, control: LocateControl) => void) | undefined; 48 | onLocationOutsideMapBounds?: ((control: LocateControl) => void) | undefined; 49 | showPopup?: boolean | undefined; 50 | strings?: StringsOptions | undefined; 51 | locateOptions?: LeafletLocateOptions | undefined; 52 | } 53 | 54 | export class LocateControl extends Control { 55 | constructor(locateOptions?: LocateOptions); 56 | 57 | onAdd(map: Map): HTMLElement; 58 | 59 | start(): void; 60 | 61 | stop(): void; 62 | 63 | stopFollowing(): void; 64 | 65 | setView(): void; 66 | } 67 | -------------------------------------------------------------------------------- /dist/L.Control.Locate.esm.js: -------------------------------------------------------------------------------- 1 | import { Marker, setOptions, divIcon, Control, DomUtil, extend, LayerGroup, DomEvent, Util, circle } from 'leaflet'; 2 | 3 | /*! 4 | Copyright (c) 2016 Dominik Moritz 5 | 6 | This file is part of the leaflet locate control. It is licensed under the MIT license. 7 | You can find the project at: https://github.com/domoritz/leaflet-locatecontrol 8 | */ 9 | const addClasses = (el, names) => { 10 | names.split(" ").forEach((className) => { 11 | el.classList.add(className); 12 | }); 13 | }; 14 | 15 | const removeClasses = (el, names) => { 16 | names.split(" ").forEach((className) => { 17 | el.classList.remove(className); 18 | }); 19 | }; 20 | 21 | /** 22 | * Compatible with Circle but a true marker instead of a path 23 | */ 24 | const LocationMarker = Marker.extend({ 25 | initialize(latlng, options) { 26 | setOptions(this, options); 27 | this._latlng = latlng; 28 | this.createIcon(); 29 | }, 30 | 31 | /** 32 | * Create a styled circle location marker 33 | */ 34 | createIcon() { 35 | const opt = this.options; 36 | 37 | const style = [ 38 | ["stroke", opt.color], 39 | ["stroke-width", opt.weight], 40 | ["fill", opt.fillColor], 41 | ["fill-opacity", opt.fillOpacity], 42 | ["opacity", opt.opacity] 43 | ] 44 | .filter(([k,v]) => v !== undefined) 45 | .map(([k,v]) => `${k}="${v}"`) 46 | .join(" "); 47 | 48 | const icon = this._getIconSVG(opt, style); 49 | 50 | this._locationIcon = divIcon({ 51 | className: icon.className, 52 | html: icon.svg, 53 | iconSize: [icon.w, icon.h] 54 | }); 55 | 56 | this.setIcon(this._locationIcon); 57 | }, 58 | 59 | /** 60 | * Return the raw svg for the shape 61 | * 62 | * Split so can be easily overridden 63 | */ 64 | _getIconSVG(options, style) { 65 | const r = options.radius; 66 | const w = options.weight; 67 | const s = r + w; 68 | const s2 = s * 2; 69 | const svg = 70 | `` + 71 | ``; 72 | return { 73 | className: "leaflet-control-locate-location", 74 | svg, 75 | w: s2, 76 | h: s2 77 | }; 78 | }, 79 | 80 | setStyle(style) { 81 | setOptions(this, style); 82 | this.createIcon(); 83 | } 84 | }); 85 | 86 | const CompassMarker = LocationMarker.extend({ 87 | initialize(latlng, heading, options) { 88 | setOptions(this, options); 89 | this._latlng = latlng; 90 | this._heading = heading; 91 | this.createIcon(); 92 | }, 93 | 94 | setHeading(heading) { 95 | this._heading = heading; 96 | }, 97 | 98 | /** 99 | * Create a styled arrow compass marker 100 | */ 101 | _getIconSVG(options, style) { 102 | const r = options.radius; 103 | const w = options.width + options.weight; 104 | const h = (r + options.depth + options.weight) * 2; 105 | const path = `M0,0 l${options.width / 2},${options.depth} l-${w},0 z`; 106 | const svg = 107 | `` + 108 | ``; 109 | return { 110 | className: "leaflet-control-locate-heading", 111 | svg, 112 | w, 113 | h 114 | }; 115 | } 116 | }); 117 | 118 | const LocateControl = Control.extend({ 119 | options: { 120 | /** Position of the control */ 121 | position: "topleft", 122 | /** The layer that the user's location should be drawn on. By default creates a new layer. */ 123 | layer: undefined, 124 | /** 125 | * Automatically sets the map view (zoom and pan) to the user's location as it updates. 126 | * While the map is following the user's location, the control is in the `following` state, 127 | * which changes the style of the control and the circle marker. 128 | * 129 | * Possible values: 130 | * - false: never updates the map view when location changes. 131 | * - 'once': set the view when the location is first determined 132 | * - 'always': always updates the map view when location changes. 133 | * The map view follows the user's location. 134 | * - 'untilPan': like 'always', except stops updating the 135 | * view if the user has manually panned the map. 136 | * The map view follows the user's location until she pans. 137 | * - 'untilPanOrZoom': (default) like 'always', except stops updating the 138 | * view if the user has manually panned the map. 139 | * The map view follows the user's location until she pans. 140 | */ 141 | setView: "untilPanOrZoom", 142 | /** Keep the current map zoom level when setting the view and only pan. */ 143 | keepCurrentZoomLevel: false, 144 | /** After activating the plugin by clicking on the icon, zoom to the selected zoom level, even when keepCurrentZoomLevel is true. Set to 'false' to disable this feature. */ 145 | initialZoomLevel: false, 146 | /** 147 | * This callback can be used to override the viewport tracking 148 | * This function should return a LatLngBounds object. 149 | * 150 | * For example to extend the viewport to ensure that a particular LatLng is visible: 151 | * 152 | * getLocationBounds: function(locationEvent) { 153 | * return locationEvent.bounds.extend([-33.873085, 151.219273]); 154 | * }, 155 | */ 156 | getLocationBounds(locationEvent) { 157 | return locationEvent.bounds; 158 | }, 159 | /** Smooth pan and zoom to the location of the marker. Only works in Leaflet 1.0+. */ 160 | flyTo: false, 161 | /** 162 | * The user location can be inside and outside the current view when the user clicks on the 163 | * control that is already active. Both cases can be configures separately. 164 | * Possible values are: 165 | * - 'setView': zoom and pan to the current location 166 | * - 'stop': stop locating and remove the location marker 167 | */ 168 | clickBehavior: { 169 | /** What should happen if the user clicks on the control while the location is within the current view. */ 170 | inView: "stop", 171 | /** What should happen if the user clicks on the control while the location is outside the current view. */ 172 | outOfView: "setView", 173 | /** 174 | * What should happen if the user clicks on the control while the location is within the current view 175 | * and we could be following but are not. Defaults to a special value which inherits from 'inView'; 176 | */ 177 | inViewNotFollowing: "inView" 178 | }, 179 | /** 180 | * If set, save the map bounds just before centering to the user's 181 | * location. When control is disabled, set the view back to the 182 | * bounds that were saved. 183 | */ 184 | returnToPrevBounds: false, 185 | /** 186 | * Keep a cache of the location after the user deactivates the control. If set to false, the user has to wait 187 | * until the locate API returns a new location before they see where they are again. 188 | */ 189 | cacheLocation: true, 190 | /** If set, a circle that shows the location accuracy is drawn. */ 191 | drawCircle: true, 192 | /** If set, the marker at the users' location is drawn. */ 193 | drawMarker: true, 194 | /** If set and supported then show the compass heading */ 195 | showCompass: true, 196 | /** The class to be used to create the marker. For example L.CircleMarker or L.Marker */ 197 | markerClass: LocationMarker, 198 | /** The class us be used to create the compass bearing arrow */ 199 | compassClass: CompassMarker, 200 | /** Accuracy circle style properties. NOTE these styles should match the css animations styles */ 201 | circleStyle: { 202 | className: "leaflet-control-locate-circle", 203 | color: "#136AEC", 204 | fillColor: "#136AEC", 205 | fillOpacity: 0.15, 206 | weight: 0 207 | }, 208 | /** Inner marker style properties. Only works if your marker class supports `setStyle`. */ 209 | markerStyle: { 210 | className: "leaflet-control-locate-marker", 211 | color: "#fff", 212 | fillColor: "#2A93EE", 213 | fillOpacity: 1, 214 | weight: 3, 215 | opacity: 1, 216 | radius: 9 217 | }, 218 | /** Compass */ 219 | compassStyle: { 220 | fillColor: "#2A93EE", 221 | fillOpacity: 1, 222 | weight: 0, 223 | color: "#fff", 224 | opacity: 1, 225 | radius: 9, // How far is the arrow from the center of the marker 226 | width: 9, // Width of the arrow 227 | depth: 6 // Length of the arrow 228 | }, 229 | /** 230 | * Changes to accuracy circle and inner marker while following. 231 | * It is only necessary to provide the properties that should change. 232 | */ 233 | followCircleStyle: {}, 234 | followMarkerStyle: { 235 | // color: '#FFA500', 236 | // fillColor: '#FFB000' 237 | }, 238 | followCompassStyle: {}, 239 | /** The CSS class for the icon. For example fa-location-arrow or fa-map-marker */ 240 | icon: "leaflet-control-locate-location-arrow", 241 | iconLoading: "leaflet-control-locate-spinner", 242 | /** The element to be created for icons. For example span or i */ 243 | iconElementTag: "span", 244 | /** The element to be created for the text. For example small or span */ 245 | textElementTag: "small", 246 | /** Padding around the accuracy circle. */ 247 | circlePadding: [0, 0], 248 | /** Use metric units. */ 249 | metric: true, 250 | /** 251 | * This callback can be used in case you would like to override button creation behavior. 252 | * This is useful for DOM manipulation frameworks such as angular etc. 253 | * This function should return an object with HtmlElement for the button (link property) and the icon (icon property). 254 | */ 255 | createButtonCallback(container, options) { 256 | const link = DomUtil.create("a", "leaflet-bar-part leaflet-bar-part-single", container); 257 | link.title = options.strings.title; 258 | link.href = "#"; 259 | link.setAttribute("role", "button"); 260 | const icon = DomUtil.create(options.iconElementTag, options.icon, link); 261 | 262 | if (options.strings.text !== undefined) { 263 | const text = DomUtil.create(options.textElementTag, "leaflet-locate-text", link); 264 | text.textContent = options.strings.text; 265 | link.classList.add("leaflet-locate-text-active"); 266 | link.parentNode.style.display = "flex"; 267 | if (options.icon.length > 0) { 268 | icon.classList.add("leaflet-locate-icon"); 269 | } 270 | } 271 | 272 | return { link, icon }; 273 | }, 274 | /** This event is called in case of any location error that is not a time out error. */ 275 | onLocationError(err, control) { 276 | alert(err.message); 277 | }, 278 | /** 279 | * This event is called when the user's location is outside the bounds set on the map. 280 | * The event is called repeatedly when the location changes. 281 | */ 282 | onLocationOutsideMapBounds(control) { 283 | control.stop(); 284 | alert(control.options.strings.outsideMapBoundsMsg); 285 | }, 286 | /** Display a pop-up when the user click on the inner marker. */ 287 | showPopup: true, 288 | strings: { 289 | title: "Show me where I am", 290 | metersUnit: "meters", 291 | feetUnit: "feet", 292 | popup: "You are within {distance} {unit} from this point", 293 | outsideMapBoundsMsg: "You seem located outside the boundaries of the map" 294 | }, 295 | /** The default options passed to leaflets locate method. */ 296 | locateOptions: { 297 | maxZoom: Infinity, 298 | watch: true, // if you overwrite this, visualization cannot be updated 299 | setView: false // have to set this to false because we have to 300 | // do setView manually 301 | } 302 | }, 303 | 304 | initialize(options) { 305 | // set default options if nothing is set (merge one step deep) 306 | for (const i in options) { 307 | if (typeof this.options[i] === "object") { 308 | extend(this.options[i], options[i]); 309 | } else { 310 | this.options[i] = options[i]; 311 | } 312 | } 313 | 314 | // extend the follow marker style and circle from the normal style 315 | this.options.followMarkerStyle = extend({}, this.options.markerStyle, this.options.followMarkerStyle); 316 | this.options.followCircleStyle = extend({}, this.options.circleStyle, this.options.followCircleStyle); 317 | this.options.followCompassStyle = extend({}, this.options.compassStyle, this.options.followCompassStyle); 318 | }, 319 | 320 | /** 321 | * Add control to map. Returns the container for the control. 322 | */ 323 | onAdd(map) { 324 | const container = DomUtil.create("div", "leaflet-control-locate leaflet-bar leaflet-control"); 325 | this._container = container; 326 | this._map = map; 327 | this._layer = this.options.layer || new LayerGroup(); 328 | this._layer.addTo(map); 329 | this._event = undefined; 330 | this._compassHeading = null; 331 | this._prevBounds = null; 332 | 333 | const linkAndIcon = this.options.createButtonCallback(container, this.options); 334 | this._link = linkAndIcon.link; 335 | this._icon = linkAndIcon.icon; 336 | 337 | DomEvent.on( 338 | this._link, 339 | "click", 340 | function (ev) { 341 | DomEvent.stopPropagation(ev); 342 | DomEvent.preventDefault(ev); 343 | this._onClick(); 344 | }, 345 | this 346 | ).on(this._link, "dblclick", DomEvent.stopPropagation); 347 | 348 | this._resetVariables(); 349 | 350 | this._map.on("unload", this._unload, this); 351 | 352 | return container; 353 | }, 354 | 355 | /** 356 | * This method is called when the user clicks on the control. 357 | */ 358 | _onClick() { 359 | this._justClicked = true; 360 | const wasFollowing = this._isFollowing(); 361 | this._userPanned = false; 362 | this._userZoomed = false; 363 | 364 | if (this._active && !this._event) { 365 | // click while requesting 366 | this.stop(); 367 | } else if (this._active) { 368 | const behaviors = this.options.clickBehavior; 369 | let behavior = behaviors.outOfView; 370 | if (this._map.getBounds().contains(this._event.latlng)) { 371 | behavior = wasFollowing ? behaviors.inView : behaviors.inViewNotFollowing; 372 | } 373 | 374 | // Allow inheriting from another behavior 375 | if (behaviors[behavior]) { 376 | behavior = behaviors[behavior]; 377 | } 378 | 379 | switch (behavior) { 380 | case "setView": 381 | this.setView(); 382 | break; 383 | case "stop": 384 | this.stop(); 385 | if (this.options.returnToPrevBounds) { 386 | const f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds; 387 | f.bind(this._map)(this._prevBounds); 388 | } 389 | break; 390 | } 391 | } else { 392 | if (this.options.returnToPrevBounds) { 393 | this._prevBounds = this._map.getBounds(); 394 | } 395 | this.start(); 396 | } 397 | 398 | this._updateContainerStyle(); 399 | }, 400 | 401 | /** 402 | * Starts the plugin: 403 | * - activates the engine 404 | * - draws the marker (if coordinates available) 405 | */ 406 | start() { 407 | this._activate(); 408 | 409 | if (this._event) { 410 | this._drawMarker(this._map); 411 | 412 | // if we already have a location but the user clicked on the control 413 | if (this.options.setView) { 414 | this.setView(); 415 | } 416 | } 417 | this._updateContainerStyle(); 418 | }, 419 | 420 | /** 421 | * Stops the plugin: 422 | * - deactivates the engine 423 | * - reinitializes the button 424 | * - removes the marker 425 | */ 426 | stop() { 427 | this._deactivate(); 428 | 429 | this._cleanClasses(); 430 | this._resetVariables(); 431 | 432 | this._removeMarker(); 433 | }, 434 | 435 | /** 436 | * Keep the control active but stop following the location 437 | */ 438 | stopFollowing() { 439 | this._userPanned = true; 440 | this._updateContainerStyle(); 441 | this._drawMarker(); 442 | }, 443 | 444 | /** 445 | * This method launches the location engine. 446 | * It is called before the marker is updated, 447 | * event if it does not mean that the event will be ready. 448 | * 449 | * Override it if you want to add more functionalities. 450 | * It should set the this._active to true and do nothing if 451 | * this._active is true. 452 | */ 453 | _activate() { 454 | if (this._active || !this._map) { 455 | return; 456 | } 457 | 458 | this._map.locate(this.options.locateOptions); 459 | this._map.fire("locateactivate", this); 460 | this._active = true; 461 | 462 | // bind event listeners 463 | this._map.on("locationfound", this._onLocationFound, this); 464 | this._map.on("locationerror", this._onLocationError, this); 465 | this._map.on("dragstart", this._onDrag, this); 466 | this._map.on("zoomstart", this._onZoom, this); 467 | this._map.on("zoomend", this._onZoomEnd, this); 468 | if (this.options.showCompass) { 469 | const oriAbs = "ondeviceorientationabsolute" in window; 470 | if (oriAbs || "ondeviceorientation" in window) { 471 | const _this = this; 472 | const deviceorientation = function () { 473 | DomEvent.on(window, oriAbs ? "deviceorientationabsolute" : "deviceorientation", _this._onDeviceOrientation, _this); 474 | }; 475 | if (DeviceOrientationEvent && typeof DeviceOrientationEvent.requestPermission === "function") { 476 | DeviceOrientationEvent.requestPermission().then(function (permissionState) { 477 | if (permissionState === "granted") { 478 | deviceorientation(); 479 | } 480 | }); 481 | } else { 482 | deviceorientation(); 483 | } 484 | } 485 | } 486 | }, 487 | 488 | /** 489 | * Called to stop the location engine. 490 | * 491 | * Override it to shutdown any functionalities you added on start. 492 | */ 493 | _deactivate() { 494 | if (!this._active || !this._map) { 495 | return; 496 | } 497 | 498 | this._map.stopLocate(); 499 | this._map.fire("locatedeactivate", this); 500 | this._active = false; 501 | 502 | if (!this.options.cacheLocation) { 503 | this._event = undefined; 504 | } 505 | 506 | // unbind event listeners 507 | this._map.off("locationfound", this._onLocationFound, this); 508 | this._map.off("locationerror", this._onLocationError, this); 509 | this._map.off("dragstart", this._onDrag, this); 510 | this._map.off("zoomstart", this._onZoom, this); 511 | this._map.off("zoomend", this._onZoomEnd, this); 512 | if (this.options.showCompass) { 513 | this._compassHeading = null; 514 | if ("ondeviceorientationabsolute" in window) { 515 | DomEvent.off(window, "deviceorientationabsolute", this._onDeviceOrientation, this); 516 | } else if ("ondeviceorientation" in window) { 517 | DomEvent.off(window, "deviceorientation", this._onDeviceOrientation, this); 518 | } 519 | } 520 | }, 521 | 522 | /** 523 | * Zoom (unless we should keep the zoom level) and an to the current view. 524 | */ 525 | setView() { 526 | this._drawMarker(); 527 | if (this._isOutsideMapBounds()) { 528 | this._event = undefined; // clear the current location so we can get back into the bounds 529 | this.options.onLocationOutsideMapBounds(this); 530 | } else { 531 | if (this._justClicked && this.options.initialZoomLevel !== false) { 532 | let f = this.options.flyTo ? this._map.flyTo : this._map.setView; 533 | f.bind(this._map)([this._event.latitude, this._event.longitude], this.options.initialZoomLevel); 534 | } else if (this.options.keepCurrentZoomLevel) { 535 | let f = this.options.flyTo ? this._map.flyTo : this._map.panTo; 536 | f.bind(this._map)([this._event.latitude, this._event.longitude]); 537 | } else { 538 | let f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds; 539 | // Ignore zoom events while setting the viewport as these would stop following 540 | this._ignoreEvent = true; 541 | f.bind(this._map)(this.options.getLocationBounds(this._event), { 542 | padding: this.options.circlePadding, 543 | maxZoom: this.options.initialZoomLevel || this.options.locateOptions.maxZoom 544 | }); 545 | Util.requestAnimFrame(function () { 546 | // Wait until after the next animFrame because the flyTo can be async 547 | this._ignoreEvent = false; 548 | }, this); 549 | } 550 | } 551 | }, 552 | 553 | /** 554 | * 555 | */ 556 | _drawCompass() { 557 | if (!this._event) { 558 | return; 559 | } 560 | 561 | const latlng = this._event.latlng; 562 | 563 | if (this.options.showCompass && latlng && this._compassHeading !== null) { 564 | const cStyle = this._isFollowing() ? this.options.followCompassStyle : this.options.compassStyle; 565 | if (!this._compass) { 566 | this._compass = new this.options.compassClass(latlng, this._compassHeading, cStyle).addTo(this._layer); 567 | } else { 568 | this._compass.setLatLng(latlng); 569 | this._compass.setHeading(this._compassHeading); 570 | // If the compassClass can be updated with setStyle, update it. 571 | if (this._compass.setStyle) { 572 | this._compass.setStyle(cStyle); 573 | } 574 | } 575 | // 576 | } 577 | if (this._compass && (!this.options.showCompass || this._compassHeading === null)) { 578 | this._compass.removeFrom(this._layer); 579 | this._compass = null; 580 | } 581 | }, 582 | 583 | /** 584 | * Draw the marker and accuracy circle on the map. 585 | * 586 | * Uses the event retrieved from onLocationFound from the map. 587 | */ 588 | _drawMarker() { 589 | if (this._event.accuracy === undefined) { 590 | this._event.accuracy = 0; 591 | } 592 | 593 | const radius = this._event.accuracy; 594 | const latlng = this._event.latlng; 595 | 596 | // circle with the radius of the location's accuracy 597 | if (this.options.drawCircle) { 598 | const style = this._isFollowing() ? this.options.followCircleStyle : this.options.circleStyle; 599 | 600 | if (!this._circle) { 601 | this._circle = circle(latlng, radius, style).addTo(this._layer); 602 | } else { 603 | this._circle.setLatLng(latlng).setRadius(radius).setStyle(style); 604 | } 605 | } 606 | 607 | let distance; 608 | let unit; 609 | if (this.options.metric) { 610 | distance = radius.toFixed(0); 611 | unit = this.options.strings.metersUnit; 612 | } else { 613 | distance = (radius * 3.2808399).toFixed(0); 614 | unit = this.options.strings.feetUnit; 615 | } 616 | 617 | // small inner marker 618 | if (this.options.drawMarker) { 619 | const mStyle = this._isFollowing() ? this.options.followMarkerStyle : this.options.markerStyle; 620 | if (!this._marker) { 621 | this._marker = new this.options.markerClass(latlng, mStyle).addTo(this._layer); 622 | } else { 623 | this._marker.setLatLng(latlng); 624 | // If the markerClass can be updated with setStyle, update it. 625 | if (this._marker.setStyle) { 626 | this._marker.setStyle(mStyle); 627 | } 628 | } 629 | } 630 | 631 | this._drawCompass(); 632 | 633 | const t = this.options.strings.popup; 634 | function getPopupText() { 635 | if (typeof t === "string") { 636 | return Util.template(t, { distance, unit }); 637 | } else if (typeof t === "function") { 638 | return t({ distance, unit }); 639 | } else { 640 | return t; 641 | } 642 | } 643 | if (this.options.showPopup && t && this._marker) { 644 | this._marker.bindPopup(getPopupText())._popup.setLatLng(latlng); 645 | } 646 | if (this.options.showPopup && t && this._compass) { 647 | this._compass.bindPopup(getPopupText())._popup.setLatLng(latlng); 648 | } 649 | }, 650 | 651 | /** 652 | * Remove the marker from map. 653 | */ 654 | _removeMarker() { 655 | this._layer.clearLayers(); 656 | this._marker = undefined; 657 | this._circle = undefined; 658 | }, 659 | 660 | /** 661 | * Unload the plugin and all event listeners. 662 | * Kind of the opposite of onAdd. 663 | */ 664 | _unload() { 665 | this.stop(); 666 | // May become undefined during HMR 667 | if (this._map) { 668 | this._map.off("unload", this._unload, this); 669 | } 670 | }, 671 | 672 | /** 673 | * Sets the compass heading 674 | */ 675 | _setCompassHeading(angle) { 676 | if (!isNaN(parseFloat(angle)) && isFinite(angle)) { 677 | angle = Math.round(angle); 678 | 679 | this._compassHeading = angle; 680 | Util.requestAnimFrame(this._drawCompass, this); 681 | } else { 682 | this._compassHeading = null; 683 | } 684 | }, 685 | 686 | /** 687 | * If the compass fails calibration just fail safely and remove the compass 688 | */ 689 | _onCompassNeedsCalibration() { 690 | this._setCompassHeading(); 691 | }, 692 | 693 | /** 694 | * Process and normalise compass events 695 | */ 696 | _onDeviceOrientation(e) { 697 | if (!this._active) { 698 | return; 699 | } 700 | 701 | if (e.webkitCompassHeading) { 702 | // iOS 703 | this._setCompassHeading(e.webkitCompassHeading); 704 | } else if (e.absolute && e.alpha) { 705 | // Android 706 | this._setCompassHeading(360 - e.alpha); 707 | } 708 | }, 709 | 710 | /** 711 | * Calls deactivate and dispatches an error. 712 | */ 713 | _onLocationError(err) { 714 | // ignore time out error if the location is watched 715 | if (err.code == 3 && this.options.locateOptions.watch) { 716 | return; 717 | } 718 | 719 | this.stop(); 720 | this.options.onLocationError(err, this); 721 | }, 722 | 723 | /** 724 | * Stores the received event and updates the marker. 725 | */ 726 | _onLocationFound(e) { 727 | // no need to do anything if the location has not changed 728 | if (this._event && this._event.latlng.lat === e.latlng.lat && this._event.latlng.lng === e.latlng.lng && this._event.accuracy === e.accuracy) { 729 | return; 730 | } 731 | 732 | if (!this._active) { 733 | // we may have a stray event 734 | return; 735 | } 736 | 737 | this._event = e; 738 | 739 | this._drawMarker(); 740 | this._updateContainerStyle(); 741 | 742 | switch (this.options.setView) { 743 | case "once": 744 | if (this._justClicked) { 745 | this.setView(); 746 | } 747 | break; 748 | case "untilPan": 749 | if (!this._userPanned) { 750 | this.setView(); 751 | } 752 | break; 753 | case "untilPanOrZoom": 754 | if (!this._userPanned && !this._userZoomed) { 755 | this.setView(); 756 | } 757 | break; 758 | case "always": 759 | this.setView(); 760 | break; 761 | } 762 | 763 | this._justClicked = false; 764 | }, 765 | 766 | /** 767 | * When the user drags. Need a separate event so we can bind and unbind event listeners. 768 | */ 769 | _onDrag() { 770 | // only react to drags once we have a location 771 | if (this._event && !this._ignoreEvent) { 772 | this._userPanned = true; 773 | this._updateContainerStyle(); 774 | this._drawMarker(); 775 | } 776 | }, 777 | 778 | /** 779 | * When the user zooms. Need a separate event so we can bind and unbind event listeners. 780 | */ 781 | _onZoom() { 782 | // only react to drags once we have a location 783 | if (this._event && !this._ignoreEvent) { 784 | this._userZoomed = true; 785 | this._updateContainerStyle(); 786 | this._drawMarker(); 787 | } 788 | }, 789 | 790 | /** 791 | * After a zoom ends update the compass and handle sideways zooms 792 | */ 793 | _onZoomEnd() { 794 | if (this._event) { 795 | this._drawCompass(); 796 | } 797 | 798 | if (this._event && !this._ignoreEvent) { 799 | // If we have zoomed in and out and ended up sideways treat it as a pan 800 | if (this._marker && !this._map.getBounds().pad(-0.3).contains(this._marker.getLatLng())) { 801 | this._userPanned = true; 802 | this._updateContainerStyle(); 803 | this._drawMarker(); 804 | } 805 | } 806 | }, 807 | 808 | /** 809 | * Compute whether the map is following the user location with pan and zoom. 810 | */ 811 | _isFollowing() { 812 | if (!this._active) { 813 | return false; 814 | } 815 | 816 | if (this.options.setView === "always") { 817 | return true; 818 | } else if (this.options.setView === "untilPan") { 819 | return !this._userPanned; 820 | } else if (this.options.setView === "untilPanOrZoom") { 821 | return !this._userPanned && !this._userZoomed; 822 | } 823 | }, 824 | 825 | /** 826 | * Check if location is in map bounds 827 | */ 828 | _isOutsideMapBounds() { 829 | if (this._event === undefined) { 830 | return false; 831 | } 832 | return this._map.options.maxBounds && !this._map.options.maxBounds.contains(this._event.latlng); 833 | }, 834 | 835 | /** 836 | * Toggles button class between following and active. 837 | */ 838 | _updateContainerStyle() { 839 | if (!this._container) { 840 | return; 841 | } 842 | 843 | if (this._active && !this._event) { 844 | // active but don't have a location yet 845 | this._setClasses("requesting"); 846 | } else if (this._isFollowing()) { 847 | this._setClasses("following"); 848 | } else if (this._active) { 849 | this._setClasses("active"); 850 | } else { 851 | this._cleanClasses(); 852 | } 853 | }, 854 | 855 | /** 856 | * Sets the CSS classes for the state. 857 | */ 858 | _setClasses(state) { 859 | if (state == "requesting") { 860 | removeClasses(this._container, "active following"); 861 | addClasses(this._container, "requesting"); 862 | 863 | removeClasses(this._icon, this.options.icon); 864 | addClasses(this._icon, this.options.iconLoading); 865 | } else if (state == "active") { 866 | removeClasses(this._container, "requesting following"); 867 | addClasses(this._container, "active"); 868 | 869 | removeClasses(this._icon, this.options.iconLoading); 870 | addClasses(this._icon, this.options.icon); 871 | } else if (state == "following") { 872 | removeClasses(this._container, "requesting"); 873 | addClasses(this._container, "active following"); 874 | 875 | removeClasses(this._icon, this.options.iconLoading); 876 | addClasses(this._icon, this.options.icon); 877 | } 878 | }, 879 | 880 | /** 881 | * Removes all classes from button. 882 | */ 883 | _cleanClasses() { 884 | DomUtil.removeClass(this._container, "requesting"); 885 | DomUtil.removeClass(this._container, "active"); 886 | DomUtil.removeClass(this._container, "following"); 887 | 888 | removeClasses(this._icon, this.options.iconLoading); 889 | addClasses(this._icon, this.options.icon); 890 | }, 891 | 892 | /** 893 | * Reinitializes state variables. 894 | */ 895 | _resetVariables() { 896 | // whether locate is active or not 897 | this._active = false; 898 | 899 | // true if the control was clicked for the first time 900 | // we need this so we can pan and zoom once we have the location 901 | this._justClicked = false; 902 | 903 | // true if the user has panned the map after clicking the control 904 | this._userPanned = false; 905 | 906 | // true if the user has zoomed the map after clicking the control 907 | this._userZoomed = false; 908 | } 909 | }); 910 | 911 | function locate(options) { 912 | return new LocateControl(options); 913 | } 914 | 915 | export { CompassMarker, LocateControl, LocationMarker, locate }; 916 | -------------------------------------------------------------------------------- /dist/L.Control.Locate.mapbox.css: -------------------------------------------------------------------------------- 1 | .leaflet-control-locate a { 2 | cursor: pointer; 3 | } 4 | .leaflet-control-locate a .leaflet-control-locate-location-arrow { 5 | display: inline-block; 6 | width: 16px; 7 | height: 16px; 8 | margin: 7px; 9 | background-image: url('data:image/svg+xml;charset=UTF-8,'); 10 | } 11 | .leaflet-control-locate a .leaflet-control-locate-spinner { 12 | display: inline-block; 13 | width: 16px; 14 | height: 16px; 15 | margin: 7px; 16 | background-image: url('data:image/svg+xml;charset=UTF-8,'); 17 | animation: leaflet-control-locate-spin 2s linear infinite; 18 | } 19 | .leaflet-control-locate.active a .leaflet-control-locate-location-arrow { 20 | background-image: url('data:image/svg+xml;charset=UTF-8,'); 21 | } 22 | .leaflet-control-locate.following a .leaflet-control-locate-location-arrow { 23 | background-image: url('data:image/svg+xml;charset=UTF-8,'); 24 | } 25 | 26 | .leaflet-touch .leaflet-bar .leaflet-locate-text-active { 27 | width: 100%; 28 | max-width: 200px; 29 | text-overflow: ellipsis; 30 | white-space: nowrap; 31 | overflow: hidden; 32 | padding: 0 10px; 33 | } 34 | .leaflet-touch .leaflet-bar .leaflet-locate-text-active .leaflet-locate-icon { 35 | padding: 0 5px 0 0; 36 | } 37 | 38 | .leaflet-control-locate-location circle { 39 | animation: leaflet-control-locate-throb 4s ease infinite; 40 | } 41 | 42 | @keyframes leaflet-control-locate-throb { 43 | 0% { 44 | stroke-width: 1; 45 | } 46 | 50% { 47 | stroke-width: 3; 48 | transform: scale(0.8, 0.8); 49 | } 50 | 100% { 51 | stroke-width: 1; 52 | } 53 | } 54 | @keyframes leaflet-control-locate-spin { 55 | 0% { 56 | transform: rotate(0deg); 57 | } 58 | 100% { 59 | transform: rotate(360deg); 60 | } 61 | } 62 | /* Mapbox specific adjustments */ 63 | .leaflet-control-locate a .leaflet-control-locate-location-arrow, 64 | .leaflet-control-locate a .leaflet-control-locate-spinner { 65 | margin: 5px; 66 | } -------------------------------------------------------------------------------- /dist/L.Control.Locate.mapbox.css.map: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domoritz/leaflet-locatecontrol/70b5bd24cdb5fa1debddbf9cd3d37f957278873e/dist/L.Control.Locate.mapbox.css.map -------------------------------------------------------------------------------- /dist/L.Control.Locate.mapbox.min.css: -------------------------------------------------------------------------------- 1 | .leaflet-control-locate a{cursor:pointer}.leaflet-control-locate a .leaflet-control-locate-location-arrow{display:inline-block;width:16px;height:16px;margin:7px;background-image:url('data:image/svg+xml;charset=UTF-8,')}.leaflet-control-locate a .leaflet-control-locate-spinner{display:inline-block;width:16px;height:16px;margin:7px;background-image:url('data:image/svg+xml;charset=UTF-8,');animation:leaflet-control-locate-spin 2s linear infinite}.leaflet-control-locate.active a .leaflet-control-locate-location-arrow{background-image:url('data:image/svg+xml;charset=UTF-8,')}.leaflet-control-locate.following a .leaflet-control-locate-location-arrow{background-image:url('data:image/svg+xml;charset=UTF-8,')}.leaflet-touch .leaflet-bar .leaflet-locate-text-active{width:100%;max-width:200px;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;padding:0 10px}.leaflet-touch .leaflet-bar .leaflet-locate-text-active .leaflet-locate-icon{padding:0 5px 0 0}.leaflet-control-locate-location circle{animation:leaflet-control-locate-throb 4s ease infinite}@keyframes leaflet-control-locate-throb{0%{stroke-width:1}50%{stroke-width:3;transform:scale(0.8, 0.8)}100%{stroke-width:1}}@keyframes leaflet-control-locate-spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.leaflet-control-locate a .leaflet-control-locate-location-arrow,.leaflet-control-locate a .leaflet-control-locate-spinner{margin:5px} -------------------------------------------------------------------------------- /dist/L.Control.Locate.mapbox.min.css.map: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domoritz/leaflet-locatecontrol/70b5bd24cdb5fa1debddbf9cd3d37f957278873e/dist/L.Control.Locate.mapbox.min.css.map -------------------------------------------------------------------------------- /dist/L.Control.Locate.min.css: -------------------------------------------------------------------------------- 1 | .leaflet-control-locate a{cursor:pointer}.leaflet-control-locate a .leaflet-control-locate-location-arrow{display:inline-block;width:16px;height:16px;margin:7px;background-image:url('data:image/svg+xml;charset=UTF-8,')}.leaflet-control-locate a .leaflet-control-locate-spinner{display:inline-block;width:16px;height:16px;margin:7px;background-image:url('data:image/svg+xml;charset=UTF-8,');animation:leaflet-control-locate-spin 2s linear infinite}.leaflet-control-locate.active a .leaflet-control-locate-location-arrow{background-image:url('data:image/svg+xml;charset=UTF-8,')}.leaflet-control-locate.following a .leaflet-control-locate-location-arrow{background-image:url('data:image/svg+xml;charset=UTF-8,')}.leaflet-touch .leaflet-bar .leaflet-locate-text-active{width:100%;max-width:200px;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;padding:0 10px}.leaflet-touch .leaflet-bar .leaflet-locate-text-active .leaflet-locate-icon{padding:0 5px 0 0}.leaflet-control-locate-location circle{animation:leaflet-control-locate-throb 4s ease infinite}@keyframes leaflet-control-locate-throb{0%{stroke-width:1}50%{stroke-width:3;transform:scale(0.8, 0.8)}100%{stroke-width:1}}@keyframes leaflet-control-locate-spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}} -------------------------------------------------------------------------------- /dist/L.Control.Locate.min.css.map: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domoritz/leaflet-locatecontrol/70b5bd24cdb5fa1debddbf9cd3d37f957278873e/dist/L.Control.Locate.min.css.map -------------------------------------------------------------------------------- /dist/L.Control.Locate.min.js: -------------------------------------------------------------------------------- 1 | /*! Version: 0.84.2 2 | Copyright (c) 2016 Dominik Moritz */ 3 | !function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports,require("leaflet")):"function"==typeof define&&define.amd?define(["exports","leaflet"],i):i(((t="undefined"!=typeof globalThis?globalThis:t||self).L=t.L||{},t.L.Control=t.L.Control||{},t.L.Control.Locate={}),t.L)}(this,function(t,l){"use strict";const i=(i,t)=>{t.split(" ").forEach(t=>{i.classList.add(t)})},s=(i,t)=>{t.split(" ").forEach(t=>{i.classList.remove(t)})};var o=l.Marker.extend({initialize(t,i){l.setOptions(this,i),this._latlng=t,this.createIcon()},createIcon(){var t=this.options,i=[["stroke",t.color],["stroke-width",t.weight],["fill",t.fillColor],["fill-opacity",t.fillOpacity],["opacity",t.opacity]].filter(([,t])=>void 0!==t).map(([t,i])=>t+`="${i}"`).join(" "),t=this._getIconSVG(t,i);this._locationIcon=l.divIcon({className:t.className,html:t.svg,iconSize:[t.w,t.h]}),this.setIcon(this._locationIcon)},_getIconSVG(t,i){var s=t.radius,t=s+t.weight,o=2*t;return{className:"leaflet-control-locate-location",svg:``+``,w:o,h:o}},setStyle(t){l.setOptions(this,t),this.createIcon()}}),e=o.extend({initialize(t,i,s){l.setOptions(this,s),this._latlng=t,this._heading=i,this.createIcon()},setHeading(t){this._heading=t},_getIconSVG(t,i){var s=t.radius,o=t.width+t.weight,s=2*(s+t.depth+t.weight),t=`M0,0 l${t.width/2},${t.depth} l-${o},0 z`;return{className:"leaflet-control-locate-heading",svg:``+``,w:o,h:s}}});const n=l.Control.extend({options:{position:"topleft",layer:void 0,setView:"untilPanOrZoom",keepCurrentZoomLevel:!1,initialZoomLevel:!1,getLocationBounds(t){return t.bounds},flyTo:!1,clickBehavior:{inView:"stop",outOfView:"setView",inViewNotFollowing:"inView"},returnToPrevBounds:!1,cacheLocation:!0,drawCircle:!0,drawMarker:!0,showCompass:!0,markerClass:o,compassClass:e,circleStyle:{className:"leaflet-control-locate-circle",color:"#136AEC",fillColor:"#136AEC",fillOpacity:.15,weight:0},markerStyle:{className:"leaflet-control-locate-marker",color:"#fff",fillColor:"#2A93EE",fillOpacity:1,weight:3,opacity:1,radius:9},compassStyle:{fillColor:"#2A93EE",fillOpacity:1,weight:0,color:"#fff",opacity:1,radius:9,width:9,depth:6},followCircleStyle:{},followMarkerStyle:{},followCompassStyle:{},icon:"leaflet-control-locate-location-arrow",iconLoading:"leaflet-control-locate-spinner",iconElementTag:"span",textElementTag:"small",circlePadding:[0,0],metric:!0,createButtonCallback(t,i){var t=l.DomUtil.create("a","leaflet-bar-part leaflet-bar-part-single",t),s=(t.title=i.strings.title,t.href="#",t.setAttribute("role","button"),l.DomUtil.create(i.iconElementTag,i.icon,t));return void 0!==i.strings.text&&(l.DomUtil.create(i.textElementTag,"leaflet-locate-text",t).textContent=i.strings.text,t.classList.add("leaflet-locate-text-active"),t.parentNode.style.display="flex",0 { 14 | names.split(" ").forEach((className) => { 15 | el.classList.add(className); 16 | }); 17 | }; 18 | 19 | const removeClasses = (el, names) => { 20 | names.split(" ").forEach((className) => { 21 | el.classList.remove(className); 22 | }); 23 | }; 24 | 25 | /** 26 | * Compatible with Circle but a true marker instead of a path 27 | */ 28 | const LocationMarker = leaflet.Marker.extend({ 29 | initialize(latlng, options) { 30 | leaflet.setOptions(this, options); 31 | this._latlng = latlng; 32 | this.createIcon(); 33 | }, 34 | 35 | /** 36 | * Create a styled circle location marker 37 | */ 38 | createIcon() { 39 | const opt = this.options; 40 | 41 | const style = [ 42 | ["stroke", opt.color], 43 | ["stroke-width", opt.weight], 44 | ["fill", opt.fillColor], 45 | ["fill-opacity", opt.fillOpacity], 46 | ["opacity", opt.opacity] 47 | ] 48 | .filter(([k,v]) => v !== undefined) 49 | .map(([k,v]) => `${k}="${v}"`) 50 | .join(" "); 51 | 52 | const icon = this._getIconSVG(opt, style); 53 | 54 | this._locationIcon = leaflet.divIcon({ 55 | className: icon.className, 56 | html: icon.svg, 57 | iconSize: [icon.w, icon.h] 58 | }); 59 | 60 | this.setIcon(this._locationIcon); 61 | }, 62 | 63 | /** 64 | * Return the raw svg for the shape 65 | * 66 | * Split so can be easily overridden 67 | */ 68 | _getIconSVG(options, style) { 69 | const r = options.radius; 70 | const w = options.weight; 71 | const s = r + w; 72 | const s2 = s * 2; 73 | const svg = 74 | `` + 75 | ``; 76 | return { 77 | className: "leaflet-control-locate-location", 78 | svg, 79 | w: s2, 80 | h: s2 81 | }; 82 | }, 83 | 84 | setStyle(style) { 85 | leaflet.setOptions(this, style); 86 | this.createIcon(); 87 | } 88 | }); 89 | 90 | const CompassMarker = LocationMarker.extend({ 91 | initialize(latlng, heading, options) { 92 | leaflet.setOptions(this, options); 93 | this._latlng = latlng; 94 | this._heading = heading; 95 | this.createIcon(); 96 | }, 97 | 98 | setHeading(heading) { 99 | this._heading = heading; 100 | }, 101 | 102 | /** 103 | * Create a styled arrow compass marker 104 | */ 105 | _getIconSVG(options, style) { 106 | const r = options.radius; 107 | const w = options.width + options.weight; 108 | const h = (r + options.depth + options.weight) * 2; 109 | const path = `M0,0 l${options.width / 2},${options.depth} l-${w},0 z`; 110 | const svg = 111 | `` + 112 | ``; 113 | return { 114 | className: "leaflet-control-locate-heading", 115 | svg, 116 | w, 117 | h 118 | }; 119 | } 120 | }); 121 | 122 | const LocateControl = leaflet.Control.extend({ 123 | options: { 124 | /** Position of the control */ 125 | position: "topleft", 126 | /** The layer that the user's location should be drawn on. By default creates a new layer. */ 127 | layer: undefined, 128 | /** 129 | * Automatically sets the map view (zoom and pan) to the user's location as it updates. 130 | * While the map is following the user's location, the control is in the `following` state, 131 | * which changes the style of the control and the circle marker. 132 | * 133 | * Possible values: 134 | * - false: never updates the map view when location changes. 135 | * - 'once': set the view when the location is first determined 136 | * - 'always': always updates the map view when location changes. 137 | * The map view follows the user's location. 138 | * - 'untilPan': like 'always', except stops updating the 139 | * view if the user has manually panned the map. 140 | * The map view follows the user's location until she pans. 141 | * - 'untilPanOrZoom': (default) like 'always', except stops updating the 142 | * view if the user has manually panned the map. 143 | * The map view follows the user's location until she pans. 144 | */ 145 | setView: "untilPanOrZoom", 146 | /** Keep the current map zoom level when setting the view and only pan. */ 147 | keepCurrentZoomLevel: false, 148 | /** After activating the plugin by clicking on the icon, zoom to the selected zoom level, even when keepCurrentZoomLevel is true. Set to 'false' to disable this feature. */ 149 | initialZoomLevel: false, 150 | /** 151 | * This callback can be used to override the viewport tracking 152 | * This function should return a LatLngBounds object. 153 | * 154 | * For example to extend the viewport to ensure that a particular LatLng is visible: 155 | * 156 | * getLocationBounds: function(locationEvent) { 157 | * return locationEvent.bounds.extend([-33.873085, 151.219273]); 158 | * }, 159 | */ 160 | getLocationBounds(locationEvent) { 161 | return locationEvent.bounds; 162 | }, 163 | /** Smooth pan and zoom to the location of the marker. Only works in Leaflet 1.0+. */ 164 | flyTo: false, 165 | /** 166 | * The user location can be inside and outside the current view when the user clicks on the 167 | * control that is already active. Both cases can be configures separately. 168 | * Possible values are: 169 | * - 'setView': zoom and pan to the current location 170 | * - 'stop': stop locating and remove the location marker 171 | */ 172 | clickBehavior: { 173 | /** What should happen if the user clicks on the control while the location is within the current view. */ 174 | inView: "stop", 175 | /** What should happen if the user clicks on the control while the location is outside the current view. */ 176 | outOfView: "setView", 177 | /** 178 | * What should happen if the user clicks on the control while the location is within the current view 179 | * and we could be following but are not. Defaults to a special value which inherits from 'inView'; 180 | */ 181 | inViewNotFollowing: "inView" 182 | }, 183 | /** 184 | * If set, save the map bounds just before centering to the user's 185 | * location. When control is disabled, set the view back to the 186 | * bounds that were saved. 187 | */ 188 | returnToPrevBounds: false, 189 | /** 190 | * Keep a cache of the location after the user deactivates the control. If set to false, the user has to wait 191 | * until the locate API returns a new location before they see where they are again. 192 | */ 193 | cacheLocation: true, 194 | /** If set, a circle that shows the location accuracy is drawn. */ 195 | drawCircle: true, 196 | /** If set, the marker at the users' location is drawn. */ 197 | drawMarker: true, 198 | /** If set and supported then show the compass heading */ 199 | showCompass: true, 200 | /** The class to be used to create the marker. For example L.CircleMarker or L.Marker */ 201 | markerClass: LocationMarker, 202 | /** The class us be used to create the compass bearing arrow */ 203 | compassClass: CompassMarker, 204 | /** Accuracy circle style properties. NOTE these styles should match the css animations styles */ 205 | circleStyle: { 206 | className: "leaflet-control-locate-circle", 207 | color: "#136AEC", 208 | fillColor: "#136AEC", 209 | fillOpacity: 0.15, 210 | weight: 0 211 | }, 212 | /** Inner marker style properties. Only works if your marker class supports `setStyle`. */ 213 | markerStyle: { 214 | className: "leaflet-control-locate-marker", 215 | color: "#fff", 216 | fillColor: "#2A93EE", 217 | fillOpacity: 1, 218 | weight: 3, 219 | opacity: 1, 220 | radius: 9 221 | }, 222 | /** Compass */ 223 | compassStyle: { 224 | fillColor: "#2A93EE", 225 | fillOpacity: 1, 226 | weight: 0, 227 | color: "#fff", 228 | opacity: 1, 229 | radius: 9, // How far is the arrow from the center of the marker 230 | width: 9, // Width of the arrow 231 | depth: 6 // Length of the arrow 232 | }, 233 | /** 234 | * Changes to accuracy circle and inner marker while following. 235 | * It is only necessary to provide the properties that should change. 236 | */ 237 | followCircleStyle: {}, 238 | followMarkerStyle: { 239 | // color: '#FFA500', 240 | // fillColor: '#FFB000' 241 | }, 242 | followCompassStyle: {}, 243 | /** The CSS class for the icon. For example fa-location-arrow or fa-map-marker */ 244 | icon: "leaflet-control-locate-location-arrow", 245 | iconLoading: "leaflet-control-locate-spinner", 246 | /** The element to be created for icons. For example span or i */ 247 | iconElementTag: "span", 248 | /** The element to be created for the text. For example small or span */ 249 | textElementTag: "small", 250 | /** Padding around the accuracy circle. */ 251 | circlePadding: [0, 0], 252 | /** Use metric units. */ 253 | metric: true, 254 | /** 255 | * This callback can be used in case you would like to override button creation behavior. 256 | * This is useful for DOM manipulation frameworks such as angular etc. 257 | * This function should return an object with HtmlElement for the button (link property) and the icon (icon property). 258 | */ 259 | createButtonCallback(container, options) { 260 | const link = leaflet.DomUtil.create("a", "leaflet-bar-part leaflet-bar-part-single", container); 261 | link.title = options.strings.title; 262 | link.href = "#"; 263 | link.setAttribute("role", "button"); 264 | const icon = leaflet.DomUtil.create(options.iconElementTag, options.icon, link); 265 | 266 | if (options.strings.text !== undefined) { 267 | const text = leaflet.DomUtil.create(options.textElementTag, "leaflet-locate-text", link); 268 | text.textContent = options.strings.text; 269 | link.classList.add("leaflet-locate-text-active"); 270 | link.parentNode.style.display = "flex"; 271 | if (options.icon.length > 0) { 272 | icon.classList.add("leaflet-locate-icon"); 273 | } 274 | } 275 | 276 | return { link, icon }; 277 | }, 278 | /** This event is called in case of any location error that is not a time out error. */ 279 | onLocationError(err, control) { 280 | alert(err.message); 281 | }, 282 | /** 283 | * This event is called when the user's location is outside the bounds set on the map. 284 | * The event is called repeatedly when the location changes. 285 | */ 286 | onLocationOutsideMapBounds(control) { 287 | control.stop(); 288 | alert(control.options.strings.outsideMapBoundsMsg); 289 | }, 290 | /** Display a pop-up when the user click on the inner marker. */ 291 | showPopup: true, 292 | strings: { 293 | title: "Show me where I am", 294 | metersUnit: "meters", 295 | feetUnit: "feet", 296 | popup: "You are within {distance} {unit} from this point", 297 | outsideMapBoundsMsg: "You seem located outside the boundaries of the map" 298 | }, 299 | /** The default options passed to leaflets locate method. */ 300 | locateOptions: { 301 | maxZoom: Infinity, 302 | watch: true, // if you overwrite this, visualization cannot be updated 303 | setView: false // have to set this to false because we have to 304 | // do setView manually 305 | } 306 | }, 307 | 308 | initialize(options) { 309 | // set default options if nothing is set (merge one step deep) 310 | for (const i in options) { 311 | if (typeof this.options[i] === "object") { 312 | leaflet.extend(this.options[i], options[i]); 313 | } else { 314 | this.options[i] = options[i]; 315 | } 316 | } 317 | 318 | // extend the follow marker style and circle from the normal style 319 | this.options.followMarkerStyle = leaflet.extend({}, this.options.markerStyle, this.options.followMarkerStyle); 320 | this.options.followCircleStyle = leaflet.extend({}, this.options.circleStyle, this.options.followCircleStyle); 321 | this.options.followCompassStyle = leaflet.extend({}, this.options.compassStyle, this.options.followCompassStyle); 322 | }, 323 | 324 | /** 325 | * Add control to map. Returns the container for the control. 326 | */ 327 | onAdd(map) { 328 | const container = leaflet.DomUtil.create("div", "leaflet-control-locate leaflet-bar leaflet-control"); 329 | this._container = container; 330 | this._map = map; 331 | this._layer = this.options.layer || new leaflet.LayerGroup(); 332 | this._layer.addTo(map); 333 | this._event = undefined; 334 | this._compassHeading = null; 335 | this._prevBounds = null; 336 | 337 | const linkAndIcon = this.options.createButtonCallback(container, this.options); 338 | this._link = linkAndIcon.link; 339 | this._icon = linkAndIcon.icon; 340 | 341 | leaflet.DomEvent.on( 342 | this._link, 343 | "click", 344 | function (ev) { 345 | leaflet.DomEvent.stopPropagation(ev); 346 | leaflet.DomEvent.preventDefault(ev); 347 | this._onClick(); 348 | }, 349 | this 350 | ).on(this._link, "dblclick", leaflet.DomEvent.stopPropagation); 351 | 352 | this._resetVariables(); 353 | 354 | this._map.on("unload", this._unload, this); 355 | 356 | return container; 357 | }, 358 | 359 | /** 360 | * This method is called when the user clicks on the control. 361 | */ 362 | _onClick() { 363 | this._justClicked = true; 364 | const wasFollowing = this._isFollowing(); 365 | this._userPanned = false; 366 | this._userZoomed = false; 367 | 368 | if (this._active && !this._event) { 369 | // click while requesting 370 | this.stop(); 371 | } else if (this._active) { 372 | const behaviors = this.options.clickBehavior; 373 | let behavior = behaviors.outOfView; 374 | if (this._map.getBounds().contains(this._event.latlng)) { 375 | behavior = wasFollowing ? behaviors.inView : behaviors.inViewNotFollowing; 376 | } 377 | 378 | // Allow inheriting from another behavior 379 | if (behaviors[behavior]) { 380 | behavior = behaviors[behavior]; 381 | } 382 | 383 | switch (behavior) { 384 | case "setView": 385 | this.setView(); 386 | break; 387 | case "stop": 388 | this.stop(); 389 | if (this.options.returnToPrevBounds) { 390 | const f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds; 391 | f.bind(this._map)(this._prevBounds); 392 | } 393 | break; 394 | } 395 | } else { 396 | if (this.options.returnToPrevBounds) { 397 | this._prevBounds = this._map.getBounds(); 398 | } 399 | this.start(); 400 | } 401 | 402 | this._updateContainerStyle(); 403 | }, 404 | 405 | /** 406 | * Starts the plugin: 407 | * - activates the engine 408 | * - draws the marker (if coordinates available) 409 | */ 410 | start() { 411 | this._activate(); 412 | 413 | if (this._event) { 414 | this._drawMarker(this._map); 415 | 416 | // if we already have a location but the user clicked on the control 417 | if (this.options.setView) { 418 | this.setView(); 419 | } 420 | } 421 | this._updateContainerStyle(); 422 | }, 423 | 424 | /** 425 | * Stops the plugin: 426 | * - deactivates the engine 427 | * - reinitializes the button 428 | * - removes the marker 429 | */ 430 | stop() { 431 | this._deactivate(); 432 | 433 | this._cleanClasses(); 434 | this._resetVariables(); 435 | 436 | this._removeMarker(); 437 | }, 438 | 439 | /** 440 | * Keep the control active but stop following the location 441 | */ 442 | stopFollowing() { 443 | this._userPanned = true; 444 | this._updateContainerStyle(); 445 | this._drawMarker(); 446 | }, 447 | 448 | /** 449 | * This method launches the location engine. 450 | * It is called before the marker is updated, 451 | * event if it does not mean that the event will be ready. 452 | * 453 | * Override it if you want to add more functionalities. 454 | * It should set the this._active to true and do nothing if 455 | * this._active is true. 456 | */ 457 | _activate() { 458 | if (this._active || !this._map) { 459 | return; 460 | } 461 | 462 | this._map.locate(this.options.locateOptions); 463 | this._map.fire("locateactivate", this); 464 | this._active = true; 465 | 466 | // bind event listeners 467 | this._map.on("locationfound", this._onLocationFound, this); 468 | this._map.on("locationerror", this._onLocationError, this); 469 | this._map.on("dragstart", this._onDrag, this); 470 | this._map.on("zoomstart", this._onZoom, this); 471 | this._map.on("zoomend", this._onZoomEnd, this); 472 | if (this.options.showCompass) { 473 | const oriAbs = "ondeviceorientationabsolute" in window; 474 | if (oriAbs || "ondeviceorientation" in window) { 475 | const _this = this; 476 | const deviceorientation = function () { 477 | leaflet.DomEvent.on(window, oriAbs ? "deviceorientationabsolute" : "deviceorientation", _this._onDeviceOrientation, _this); 478 | }; 479 | if (DeviceOrientationEvent && typeof DeviceOrientationEvent.requestPermission === "function") { 480 | DeviceOrientationEvent.requestPermission().then(function (permissionState) { 481 | if (permissionState === "granted") { 482 | deviceorientation(); 483 | } 484 | }); 485 | } else { 486 | deviceorientation(); 487 | } 488 | } 489 | } 490 | }, 491 | 492 | /** 493 | * Called to stop the location engine. 494 | * 495 | * Override it to shutdown any functionalities you added on start. 496 | */ 497 | _deactivate() { 498 | if (!this._active || !this._map) { 499 | return; 500 | } 501 | 502 | this._map.stopLocate(); 503 | this._map.fire("locatedeactivate", this); 504 | this._active = false; 505 | 506 | if (!this.options.cacheLocation) { 507 | this._event = undefined; 508 | } 509 | 510 | // unbind event listeners 511 | this._map.off("locationfound", this._onLocationFound, this); 512 | this._map.off("locationerror", this._onLocationError, this); 513 | this._map.off("dragstart", this._onDrag, this); 514 | this._map.off("zoomstart", this._onZoom, this); 515 | this._map.off("zoomend", this._onZoomEnd, this); 516 | if (this.options.showCompass) { 517 | this._compassHeading = null; 518 | if ("ondeviceorientationabsolute" in window) { 519 | leaflet.DomEvent.off(window, "deviceorientationabsolute", this._onDeviceOrientation, this); 520 | } else if ("ondeviceorientation" in window) { 521 | leaflet.DomEvent.off(window, "deviceorientation", this._onDeviceOrientation, this); 522 | } 523 | } 524 | }, 525 | 526 | /** 527 | * Zoom (unless we should keep the zoom level) and an to the current view. 528 | */ 529 | setView() { 530 | this._drawMarker(); 531 | if (this._isOutsideMapBounds()) { 532 | this._event = undefined; // clear the current location so we can get back into the bounds 533 | this.options.onLocationOutsideMapBounds(this); 534 | } else { 535 | if (this._justClicked && this.options.initialZoomLevel !== false) { 536 | let f = this.options.flyTo ? this._map.flyTo : this._map.setView; 537 | f.bind(this._map)([this._event.latitude, this._event.longitude], this.options.initialZoomLevel); 538 | } else if (this.options.keepCurrentZoomLevel) { 539 | let f = this.options.flyTo ? this._map.flyTo : this._map.panTo; 540 | f.bind(this._map)([this._event.latitude, this._event.longitude]); 541 | } else { 542 | let f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds; 543 | // Ignore zoom events while setting the viewport as these would stop following 544 | this._ignoreEvent = true; 545 | f.bind(this._map)(this.options.getLocationBounds(this._event), { 546 | padding: this.options.circlePadding, 547 | maxZoom: this.options.initialZoomLevel || this.options.locateOptions.maxZoom 548 | }); 549 | leaflet.Util.requestAnimFrame(function () { 550 | // Wait until after the next animFrame because the flyTo can be async 551 | this._ignoreEvent = false; 552 | }, this); 553 | } 554 | } 555 | }, 556 | 557 | /** 558 | * 559 | */ 560 | _drawCompass() { 561 | if (!this._event) { 562 | return; 563 | } 564 | 565 | const latlng = this._event.latlng; 566 | 567 | if (this.options.showCompass && latlng && this._compassHeading !== null) { 568 | const cStyle = this._isFollowing() ? this.options.followCompassStyle : this.options.compassStyle; 569 | if (!this._compass) { 570 | this._compass = new this.options.compassClass(latlng, this._compassHeading, cStyle).addTo(this._layer); 571 | } else { 572 | this._compass.setLatLng(latlng); 573 | this._compass.setHeading(this._compassHeading); 574 | // If the compassClass can be updated with setStyle, update it. 575 | if (this._compass.setStyle) { 576 | this._compass.setStyle(cStyle); 577 | } 578 | } 579 | // 580 | } 581 | if (this._compass && (!this.options.showCompass || this._compassHeading === null)) { 582 | this._compass.removeFrom(this._layer); 583 | this._compass = null; 584 | } 585 | }, 586 | 587 | /** 588 | * Draw the marker and accuracy circle on the map. 589 | * 590 | * Uses the event retrieved from onLocationFound from the map. 591 | */ 592 | _drawMarker() { 593 | if (this._event.accuracy === undefined) { 594 | this._event.accuracy = 0; 595 | } 596 | 597 | const radius = this._event.accuracy; 598 | const latlng = this._event.latlng; 599 | 600 | // circle with the radius of the location's accuracy 601 | if (this.options.drawCircle) { 602 | const style = this._isFollowing() ? this.options.followCircleStyle : this.options.circleStyle; 603 | 604 | if (!this._circle) { 605 | this._circle = leaflet.circle(latlng, radius, style).addTo(this._layer); 606 | } else { 607 | this._circle.setLatLng(latlng).setRadius(radius).setStyle(style); 608 | } 609 | } 610 | 611 | let distance; 612 | let unit; 613 | if (this.options.metric) { 614 | distance = radius.toFixed(0); 615 | unit = this.options.strings.metersUnit; 616 | } else { 617 | distance = (radius * 3.2808399).toFixed(0); 618 | unit = this.options.strings.feetUnit; 619 | } 620 | 621 | // small inner marker 622 | if (this.options.drawMarker) { 623 | const mStyle = this._isFollowing() ? this.options.followMarkerStyle : this.options.markerStyle; 624 | if (!this._marker) { 625 | this._marker = new this.options.markerClass(latlng, mStyle).addTo(this._layer); 626 | } else { 627 | this._marker.setLatLng(latlng); 628 | // If the markerClass can be updated with setStyle, update it. 629 | if (this._marker.setStyle) { 630 | this._marker.setStyle(mStyle); 631 | } 632 | } 633 | } 634 | 635 | this._drawCompass(); 636 | 637 | const t = this.options.strings.popup; 638 | function getPopupText() { 639 | if (typeof t === "string") { 640 | return leaflet.Util.template(t, { distance, unit }); 641 | } else if (typeof t === "function") { 642 | return t({ distance, unit }); 643 | } else { 644 | return t; 645 | } 646 | } 647 | if (this.options.showPopup && t && this._marker) { 648 | this._marker.bindPopup(getPopupText())._popup.setLatLng(latlng); 649 | } 650 | if (this.options.showPopup && t && this._compass) { 651 | this._compass.bindPopup(getPopupText())._popup.setLatLng(latlng); 652 | } 653 | }, 654 | 655 | /** 656 | * Remove the marker from map. 657 | */ 658 | _removeMarker() { 659 | this._layer.clearLayers(); 660 | this._marker = undefined; 661 | this._circle = undefined; 662 | }, 663 | 664 | /** 665 | * Unload the plugin and all event listeners. 666 | * Kind of the opposite of onAdd. 667 | */ 668 | _unload() { 669 | this.stop(); 670 | // May become undefined during HMR 671 | if (this._map) { 672 | this._map.off("unload", this._unload, this); 673 | } 674 | }, 675 | 676 | /** 677 | * Sets the compass heading 678 | */ 679 | _setCompassHeading(angle) { 680 | if (!isNaN(parseFloat(angle)) && isFinite(angle)) { 681 | angle = Math.round(angle); 682 | 683 | this._compassHeading = angle; 684 | leaflet.Util.requestAnimFrame(this._drawCompass, this); 685 | } else { 686 | this._compassHeading = null; 687 | } 688 | }, 689 | 690 | /** 691 | * If the compass fails calibration just fail safely and remove the compass 692 | */ 693 | _onCompassNeedsCalibration() { 694 | this._setCompassHeading(); 695 | }, 696 | 697 | /** 698 | * Process and normalise compass events 699 | */ 700 | _onDeviceOrientation(e) { 701 | if (!this._active) { 702 | return; 703 | } 704 | 705 | if (e.webkitCompassHeading) { 706 | // iOS 707 | this._setCompassHeading(e.webkitCompassHeading); 708 | } else if (e.absolute && e.alpha) { 709 | // Android 710 | this._setCompassHeading(360 - e.alpha); 711 | } 712 | }, 713 | 714 | /** 715 | * Calls deactivate and dispatches an error. 716 | */ 717 | _onLocationError(err) { 718 | // ignore time out error if the location is watched 719 | if (err.code == 3 && this.options.locateOptions.watch) { 720 | return; 721 | } 722 | 723 | this.stop(); 724 | this.options.onLocationError(err, this); 725 | }, 726 | 727 | /** 728 | * Stores the received event and updates the marker. 729 | */ 730 | _onLocationFound(e) { 731 | // no need to do anything if the location has not changed 732 | if (this._event && this._event.latlng.lat === e.latlng.lat && this._event.latlng.lng === e.latlng.lng && this._event.accuracy === e.accuracy) { 733 | return; 734 | } 735 | 736 | if (!this._active) { 737 | // we may have a stray event 738 | return; 739 | } 740 | 741 | this._event = e; 742 | 743 | this._drawMarker(); 744 | this._updateContainerStyle(); 745 | 746 | switch (this.options.setView) { 747 | case "once": 748 | if (this._justClicked) { 749 | this.setView(); 750 | } 751 | break; 752 | case "untilPan": 753 | if (!this._userPanned) { 754 | this.setView(); 755 | } 756 | break; 757 | case "untilPanOrZoom": 758 | if (!this._userPanned && !this._userZoomed) { 759 | this.setView(); 760 | } 761 | break; 762 | case "always": 763 | this.setView(); 764 | break; 765 | } 766 | 767 | this._justClicked = false; 768 | }, 769 | 770 | /** 771 | * When the user drags. Need a separate event so we can bind and unbind event listeners. 772 | */ 773 | _onDrag() { 774 | // only react to drags once we have a location 775 | if (this._event && !this._ignoreEvent) { 776 | this._userPanned = true; 777 | this._updateContainerStyle(); 778 | this._drawMarker(); 779 | } 780 | }, 781 | 782 | /** 783 | * When the user zooms. Need a separate event so we can bind and unbind event listeners. 784 | */ 785 | _onZoom() { 786 | // only react to drags once we have a location 787 | if (this._event && !this._ignoreEvent) { 788 | this._userZoomed = true; 789 | this._updateContainerStyle(); 790 | this._drawMarker(); 791 | } 792 | }, 793 | 794 | /** 795 | * After a zoom ends update the compass and handle sideways zooms 796 | */ 797 | _onZoomEnd() { 798 | if (this._event) { 799 | this._drawCompass(); 800 | } 801 | 802 | if (this._event && !this._ignoreEvent) { 803 | // If we have zoomed in and out and ended up sideways treat it as a pan 804 | if (this._marker && !this._map.getBounds().pad(-0.3).contains(this._marker.getLatLng())) { 805 | this._userPanned = true; 806 | this._updateContainerStyle(); 807 | this._drawMarker(); 808 | } 809 | } 810 | }, 811 | 812 | /** 813 | * Compute whether the map is following the user location with pan and zoom. 814 | */ 815 | _isFollowing() { 816 | if (!this._active) { 817 | return false; 818 | } 819 | 820 | if (this.options.setView === "always") { 821 | return true; 822 | } else if (this.options.setView === "untilPan") { 823 | return !this._userPanned; 824 | } else if (this.options.setView === "untilPanOrZoom") { 825 | return !this._userPanned && !this._userZoomed; 826 | } 827 | }, 828 | 829 | /** 830 | * Check if location is in map bounds 831 | */ 832 | _isOutsideMapBounds() { 833 | if (this._event === undefined) { 834 | return false; 835 | } 836 | return this._map.options.maxBounds && !this._map.options.maxBounds.contains(this._event.latlng); 837 | }, 838 | 839 | /** 840 | * Toggles button class between following and active. 841 | */ 842 | _updateContainerStyle() { 843 | if (!this._container) { 844 | return; 845 | } 846 | 847 | if (this._active && !this._event) { 848 | // active but don't have a location yet 849 | this._setClasses("requesting"); 850 | } else if (this._isFollowing()) { 851 | this._setClasses("following"); 852 | } else if (this._active) { 853 | this._setClasses("active"); 854 | } else { 855 | this._cleanClasses(); 856 | } 857 | }, 858 | 859 | /** 860 | * Sets the CSS classes for the state. 861 | */ 862 | _setClasses(state) { 863 | if (state == "requesting") { 864 | removeClasses(this._container, "active following"); 865 | addClasses(this._container, "requesting"); 866 | 867 | removeClasses(this._icon, this.options.icon); 868 | addClasses(this._icon, this.options.iconLoading); 869 | } else if (state == "active") { 870 | removeClasses(this._container, "requesting following"); 871 | addClasses(this._container, "active"); 872 | 873 | removeClasses(this._icon, this.options.iconLoading); 874 | addClasses(this._icon, this.options.icon); 875 | } else if (state == "following") { 876 | removeClasses(this._container, "requesting"); 877 | addClasses(this._container, "active following"); 878 | 879 | removeClasses(this._icon, this.options.iconLoading); 880 | addClasses(this._icon, this.options.icon); 881 | } 882 | }, 883 | 884 | /** 885 | * Removes all classes from button. 886 | */ 887 | _cleanClasses() { 888 | leaflet.DomUtil.removeClass(this._container, "requesting"); 889 | leaflet.DomUtil.removeClass(this._container, "active"); 890 | leaflet.DomUtil.removeClass(this._container, "following"); 891 | 892 | removeClasses(this._icon, this.options.iconLoading); 893 | addClasses(this._icon, this.options.icon); 894 | }, 895 | 896 | /** 897 | * Reinitializes state variables. 898 | */ 899 | _resetVariables() { 900 | // whether locate is active or not 901 | this._active = false; 902 | 903 | // true if the control was clicked for the first time 904 | // we need this so we can pan and zoom once we have the location 905 | this._justClicked = false; 906 | 907 | // true if the user has panned the map after clicking the control 908 | this._userPanned = false; 909 | 910 | // true if the user has zoomed the map after clicking the control 911 | this._userZoomed = false; 912 | } 913 | }); 914 | 915 | function locate(options) { 916 | return new LocateControl(options); 917 | } 918 | 919 | exports.CompassMarker = CompassMarker; 920 | exports.LocateControl = LocateControl; 921 | exports.LocationMarker = LocationMarker; 922 | exports.locate = locate; 923 | 924 | Object.defineProperty(exports, '__esModule', { value: true }); 925 | 926 | })); 927 | 928 | (function() { 929 | if (typeof window !== 'undefined' && window.L) { 930 | window.L.control = window.L.control || {}; 931 | window.L.control.locate = window.L.Control.Locate.locate; 932 | } 933 | })(); 934 | 935 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const eslintPluginPrettierRecommended = require("eslint-plugin-prettier/recommended"); 2 | const globals = require("globals"); 3 | 4 | module.exports = [ 5 | { 6 | files: ["**/*.js"], 7 | languageOptions: { 8 | ecmaVersion: 2022, 9 | sourceType: "module", 10 | globals: { 11 | ...globals.browser, 12 | myCustomGlobal: "readonly" 13 | } 14 | } 15 | }, 16 | { 17 | ignores: ["*.min.js"] 18 | }, 19 | eslintPluginPrettierRecommended 20 | ]; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet.locatecontrol", 3 | "version": "0.84.2", 4 | "homepage": "https://github.com/domoritz/leaflet-locatecontrol", 5 | "description": "A useful control to geolocate the user with many options. Used by osm.org and mapbox among many others.", 6 | "main": "dist/L.Control.Locate.min.js", 7 | "unpkg": "dist/L.Control.Locate.min.js", 8 | "jsdelivr": "dist/L.Control.Locate.min.js", 9 | "module": "dist/L.Control.Locate.esm.js", 10 | "types": "dist/L.Control.Locate.d.ts", 11 | "author": "Dominik Moritz ", 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:domoritz/leaflet-locatecontrol.git" 15 | }, 16 | "keywords": [ 17 | "leaflet", 18 | "locate", 19 | "plugin" 20 | ], 21 | "license": "MIT", 22 | "readmeFilename": "README.md", 23 | "scripts": { 24 | "build": "grunt", 25 | "bump:minor": "grunt bump-only:minor && grunt && grunt bump-commit", 26 | "bump:patch": "grunt bump-only:patch && grunt && grunt bump-commit", 27 | "lint": "eslint && stylelint {**/style.css,**/*.scss} && prettier --check .", 28 | "lint:fix": "eslint --fix && stylelint --fix {**/style.css,**/*.scss} && prettier --write .", 29 | "start": "grunt connect" 30 | }, 31 | "devDependencies": { 32 | "@rollup/plugin-commonjs": "^28.0.3", 33 | "@rollup/plugin-node-resolve": "^16.0.1", 34 | "eslint": "^9.23.0", 35 | "eslint-config-prettier": "^10.1.1", 36 | "eslint-plugin-prettier": "^5.2.5", 37 | "grunt": "^1.6.1", 38 | "grunt-bump": "0.8.0", 39 | "grunt-contrib-connect": "^5.0.1", 40 | "grunt-contrib-copy": "^1.0.0", 41 | "grunt-contrib-uglify": "^5.2.2", 42 | "grunt-rollup": "^12.0.0", 43 | "grunt-sass": "^4.0.0", 44 | "leaflet": "^1.9.4", 45 | "prettier": "^3.5.3", 46 | "rollup": "^4.38.0", 47 | "sass": "^1.86.1", 48 | "stylelint": "^16.17.0", 49 | "stylelint-config-prettier-scss": "^1.0.0", 50 | "stylelint-config-recommended-scss": "^14.1.0", 51 | "stylelint-prettier": "^5.0.3", 52 | "stylelint-scss": "^6.11.1", 53 | "typescript": "^5.8.2" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domoritz/leaflet-locatecontrol/70b5bd24cdb5fa1debddbf9cd3d37f957278873e/screenshot.png -------------------------------------------------------------------------------- /src/L.Control.Locate.d.ts: -------------------------------------------------------------------------------- 1 | import { Control, Layer, Map, ControlOptions, PathOptions, MarkerOptions, LocationEvent, LatLngBounds, LocateOptions as LeafletLocateOptions } from "leaflet"; 2 | 3 | export type SetView = false | "once" | "always" | "untilPan" | "untilPanOrZoom"; 4 | export type ClickBehavior = "stop" | "setView"; 5 | 6 | export interface StringsOptions { 7 | title?: string | undefined; 8 | metersUnit?: string | undefined; 9 | feetUnit?: string | undefined; 10 | popup?: string | undefined; 11 | outsideMapBoundsMsg?: string | undefined; 12 | } 13 | 14 | export interface ClickBehaviorOptions { 15 | inView?: ClickBehavior | undefined; 16 | outOfView?: ClickBehavior | undefined; 17 | inViewNotFollowing?: ClickBehavior | "inView" | undefined; 18 | } 19 | 20 | export interface LocateOptions extends ControlOptions { 21 | layer?: Layer | undefined; 22 | setView?: SetView | undefined; 23 | keepCurrentZoomLevel?: boolean | undefined; 24 | initialZoomLevel?: number | boolean | undefined; 25 | getLocationBounds?: ((locationEvent: LocationEvent) => LatLngBounds) | undefined; 26 | flyTo?: boolean | undefined; 27 | clickBehavior?: ClickBehaviorOptions | undefined; 28 | returnToPrevBounds?: boolean | undefined; 29 | cacheLocation?: boolean | undefined; 30 | drawCircle?: boolean | undefined; 31 | drawMarker?: boolean | undefined; 32 | showCompass?: boolean | undefined; 33 | markerClass?: any; 34 | compassClass?: any; 35 | circleStyle?: PathOptions | undefined; 36 | markerStyle?: PathOptions | MarkerOptions | undefined; 37 | compassStyle?: PathOptions | undefined; 38 | followCircleStyle?: PathOptions | undefined; 39 | followMarkerStyle?: PathOptions | undefined; 40 | icon?: string | undefined; 41 | iconLoading?: string | undefined; 42 | iconElementTag?: string | undefined; 43 | textElementTag?: string | undefined; 44 | circlePadding?: number[] | undefined; 45 | metric?: boolean | undefined; 46 | createButtonCallback?: ((container: HTMLDivElement, options: LocateOptions) => { link: HTMLAnchorElement; icon: HTMLElement }) | undefined; 47 | onLocationError?: ((event: ErrorEvent, control: LocateControl) => void) | undefined; 48 | onLocationOutsideMapBounds?: ((control: LocateControl) => void) | undefined; 49 | showPopup?: boolean | undefined; 50 | strings?: StringsOptions | undefined; 51 | locateOptions?: LeafletLocateOptions | undefined; 52 | } 53 | 54 | export class LocateControl extends Control { 55 | constructor(locateOptions?: LocateOptions); 56 | 57 | onAdd(map: Map): HTMLElement; 58 | 59 | start(): void; 60 | 61 | stop(): void; 62 | 63 | stopFollowing(): void; 64 | 65 | setView(): void; 66 | } 67 | -------------------------------------------------------------------------------- /src/L.Control.Locate.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Copyright (c) 2016 Dominik Moritz 3 | 4 | This file is part of the leaflet locate control. It is licensed under the MIT license. 5 | You can find the project at: https://github.com/domoritz/leaflet-locatecontrol 6 | */ 7 | 8 | import { Control, Marker, DomUtil, setOptions, divIcon, extend, LayerGroup, circle, DomEvent, Util as LeafletUtil } from "leaflet"; 9 | const addClasses = (el, names) => { 10 | names.split(" ").forEach((className) => { 11 | el.classList.add(className); 12 | }); 13 | }; 14 | 15 | const removeClasses = (el, names) => { 16 | names.split(" ").forEach((className) => { 17 | el.classList.remove(className); 18 | }); 19 | }; 20 | 21 | /** 22 | * Compatible with Circle but a true marker instead of a path 23 | */ 24 | const LocationMarker = Marker.extend({ 25 | initialize(latlng, options) { 26 | setOptions(this, options); 27 | this._latlng = latlng; 28 | this.createIcon(); 29 | }, 30 | 31 | /** 32 | * Create a styled circle location marker 33 | */ 34 | createIcon() { 35 | const opt = this.options; 36 | 37 | const style = [ 38 | ["stroke", opt.color], 39 | ["stroke-width", opt.weight], 40 | ["fill", opt.fillColor], 41 | ["fill-opacity", opt.fillOpacity], 42 | ["opacity", opt.opacity] 43 | ] 44 | .filter(([k,v]) => v !== undefined) 45 | .map(([k,v]) => `${k}="${v}"`) 46 | .join(" "); 47 | 48 | const icon = this._getIconSVG(opt, style); 49 | 50 | this._locationIcon = divIcon({ 51 | className: icon.className, 52 | html: icon.svg, 53 | iconSize: [icon.w, icon.h] 54 | }); 55 | 56 | this.setIcon(this._locationIcon); 57 | }, 58 | 59 | /** 60 | * Return the raw svg for the shape 61 | * 62 | * Split so can be easily overridden 63 | */ 64 | _getIconSVG(options, style) { 65 | const r = options.radius; 66 | const w = options.weight; 67 | const s = r + w; 68 | const s2 = s * 2; 69 | const svg = 70 | `` + 71 | ``; 72 | return { 73 | className: "leaflet-control-locate-location", 74 | svg, 75 | w: s2, 76 | h: s2 77 | }; 78 | }, 79 | 80 | setStyle(style) { 81 | setOptions(this, style); 82 | this.createIcon(); 83 | } 84 | }); 85 | 86 | const CompassMarker = LocationMarker.extend({ 87 | initialize(latlng, heading, options) { 88 | setOptions(this, options); 89 | this._latlng = latlng; 90 | this._heading = heading; 91 | this.createIcon(); 92 | }, 93 | 94 | setHeading(heading) { 95 | this._heading = heading; 96 | }, 97 | 98 | /** 99 | * Create a styled arrow compass marker 100 | */ 101 | _getIconSVG(options, style) { 102 | const r = options.radius; 103 | const w = options.width + options.weight; 104 | const h = (r + options.depth + options.weight) * 2; 105 | const path = `M0,0 l${options.width / 2},${options.depth} l-${w},0 z`; 106 | const svg = 107 | `` + 108 | ``; 109 | return { 110 | className: "leaflet-control-locate-heading", 111 | svg, 112 | w, 113 | h 114 | }; 115 | } 116 | }); 117 | 118 | const LocateControl = Control.extend({ 119 | options: { 120 | /** Position of the control */ 121 | position: "topleft", 122 | /** The layer that the user's location should be drawn on. By default creates a new layer. */ 123 | layer: undefined, 124 | /** 125 | * Automatically sets the map view (zoom and pan) to the user's location as it updates. 126 | * While the map is following the user's location, the control is in the `following` state, 127 | * which changes the style of the control and the circle marker. 128 | * 129 | * Possible values: 130 | * - false: never updates the map view when location changes. 131 | * - 'once': set the view when the location is first determined 132 | * - 'always': always updates the map view when location changes. 133 | * The map view follows the user's location. 134 | * - 'untilPan': like 'always', except stops updating the 135 | * view if the user has manually panned the map. 136 | * The map view follows the user's location until she pans. 137 | * - 'untilPanOrZoom': (default) like 'always', except stops updating the 138 | * view if the user has manually panned the map. 139 | * The map view follows the user's location until she pans. 140 | */ 141 | setView: "untilPanOrZoom", 142 | /** Keep the current map zoom level when setting the view and only pan. */ 143 | keepCurrentZoomLevel: false, 144 | /** After activating the plugin by clicking on the icon, zoom to the selected zoom level, even when keepCurrentZoomLevel is true. Set to 'false' to disable this feature. */ 145 | initialZoomLevel: false, 146 | /** 147 | * This callback can be used to override the viewport tracking 148 | * This function should return a LatLngBounds object. 149 | * 150 | * For example to extend the viewport to ensure that a particular LatLng is visible: 151 | * 152 | * getLocationBounds: function(locationEvent) { 153 | * return locationEvent.bounds.extend([-33.873085, 151.219273]); 154 | * }, 155 | */ 156 | getLocationBounds(locationEvent) { 157 | return locationEvent.bounds; 158 | }, 159 | /** Smooth pan and zoom to the location of the marker. Only works in Leaflet 1.0+. */ 160 | flyTo: false, 161 | /** 162 | * The user location can be inside and outside the current view when the user clicks on the 163 | * control that is already active. Both cases can be configures separately. 164 | * Possible values are: 165 | * - 'setView': zoom and pan to the current location 166 | * - 'stop': stop locating and remove the location marker 167 | */ 168 | clickBehavior: { 169 | /** What should happen if the user clicks on the control while the location is within the current view. */ 170 | inView: "stop", 171 | /** What should happen if the user clicks on the control while the location is outside the current view. */ 172 | outOfView: "setView", 173 | /** 174 | * What should happen if the user clicks on the control while the location is within the current view 175 | * and we could be following but are not. Defaults to a special value which inherits from 'inView'; 176 | */ 177 | inViewNotFollowing: "inView" 178 | }, 179 | /** 180 | * If set, save the map bounds just before centering to the user's 181 | * location. When control is disabled, set the view back to the 182 | * bounds that were saved. 183 | */ 184 | returnToPrevBounds: false, 185 | /** 186 | * Keep a cache of the location after the user deactivates the control. If set to false, the user has to wait 187 | * until the locate API returns a new location before they see where they are again. 188 | */ 189 | cacheLocation: true, 190 | /** If set, a circle that shows the location accuracy is drawn. */ 191 | drawCircle: true, 192 | /** If set, the marker at the users' location is drawn. */ 193 | drawMarker: true, 194 | /** If set and supported then show the compass heading */ 195 | showCompass: true, 196 | /** The class to be used to create the marker. For example L.CircleMarker or L.Marker */ 197 | markerClass: LocationMarker, 198 | /** The class us be used to create the compass bearing arrow */ 199 | compassClass: CompassMarker, 200 | /** Accuracy circle style properties. NOTE these styles should match the css animations styles */ 201 | circleStyle: { 202 | className: "leaflet-control-locate-circle", 203 | color: "#136AEC", 204 | fillColor: "#136AEC", 205 | fillOpacity: 0.15, 206 | weight: 0 207 | }, 208 | /** Inner marker style properties. Only works if your marker class supports `setStyle`. */ 209 | markerStyle: { 210 | className: "leaflet-control-locate-marker", 211 | color: "#fff", 212 | fillColor: "#2A93EE", 213 | fillOpacity: 1, 214 | weight: 3, 215 | opacity: 1, 216 | radius: 9 217 | }, 218 | /** Compass */ 219 | compassStyle: { 220 | fillColor: "#2A93EE", 221 | fillOpacity: 1, 222 | weight: 0, 223 | color: "#fff", 224 | opacity: 1, 225 | radius: 9, // How far is the arrow from the center of the marker 226 | width: 9, // Width of the arrow 227 | depth: 6 // Length of the arrow 228 | }, 229 | /** 230 | * Changes to accuracy circle and inner marker while following. 231 | * It is only necessary to provide the properties that should change. 232 | */ 233 | followCircleStyle: {}, 234 | followMarkerStyle: { 235 | // color: '#FFA500', 236 | // fillColor: '#FFB000' 237 | }, 238 | followCompassStyle: {}, 239 | /** The CSS class for the icon. For example fa-location-arrow or fa-map-marker */ 240 | icon: "leaflet-control-locate-location-arrow", 241 | iconLoading: "leaflet-control-locate-spinner", 242 | /** The element to be created for icons. For example span or i */ 243 | iconElementTag: "span", 244 | /** The element to be created for the text. For example small or span */ 245 | textElementTag: "small", 246 | /** Padding around the accuracy circle. */ 247 | circlePadding: [0, 0], 248 | /** Use metric units. */ 249 | metric: true, 250 | /** 251 | * This callback can be used in case you would like to override button creation behavior. 252 | * This is useful for DOM manipulation frameworks such as angular etc. 253 | * This function should return an object with HtmlElement for the button (link property) and the icon (icon property). 254 | */ 255 | createButtonCallback(container, options) { 256 | const link = DomUtil.create("a", "leaflet-bar-part leaflet-bar-part-single", container); 257 | link.title = options.strings.title; 258 | link.href = "#"; 259 | link.setAttribute("role", "button"); 260 | const icon = DomUtil.create(options.iconElementTag, options.icon, link); 261 | 262 | if (options.strings.text !== undefined) { 263 | const text = DomUtil.create(options.textElementTag, "leaflet-locate-text", link); 264 | text.textContent = options.strings.text; 265 | link.classList.add("leaflet-locate-text-active"); 266 | link.parentNode.style.display = "flex"; 267 | if (options.icon.length > 0) { 268 | icon.classList.add("leaflet-locate-icon"); 269 | } 270 | } 271 | 272 | return { link, icon }; 273 | }, 274 | /** This event is called in case of any location error that is not a time out error. */ 275 | onLocationError(err, control) { 276 | alert(err.message); 277 | }, 278 | /** 279 | * This event is called when the user's location is outside the bounds set on the map. 280 | * The event is called repeatedly when the location changes. 281 | */ 282 | onLocationOutsideMapBounds(control) { 283 | control.stop(); 284 | alert(control.options.strings.outsideMapBoundsMsg); 285 | }, 286 | /** Display a pop-up when the user click on the inner marker. */ 287 | showPopup: true, 288 | strings: { 289 | title: "Show me where I am", 290 | metersUnit: "meters", 291 | feetUnit: "feet", 292 | popup: "You are within {distance} {unit} from this point", 293 | outsideMapBoundsMsg: "You seem located outside the boundaries of the map" 294 | }, 295 | /** The default options passed to leaflets locate method. */ 296 | locateOptions: { 297 | maxZoom: Infinity, 298 | watch: true, // if you overwrite this, visualization cannot be updated 299 | setView: false // have to set this to false because we have to 300 | // do setView manually 301 | } 302 | }, 303 | 304 | initialize(options) { 305 | // set default options if nothing is set (merge one step deep) 306 | for (const i in options) { 307 | if (typeof this.options[i] === "object") { 308 | extend(this.options[i], options[i]); 309 | } else { 310 | this.options[i] = options[i]; 311 | } 312 | } 313 | 314 | // extend the follow marker style and circle from the normal style 315 | this.options.followMarkerStyle = extend({}, this.options.markerStyle, this.options.followMarkerStyle); 316 | this.options.followCircleStyle = extend({}, this.options.circleStyle, this.options.followCircleStyle); 317 | this.options.followCompassStyle = extend({}, this.options.compassStyle, this.options.followCompassStyle); 318 | }, 319 | 320 | /** 321 | * Add control to map. Returns the container for the control. 322 | */ 323 | onAdd(map) { 324 | const container = DomUtil.create("div", "leaflet-control-locate leaflet-bar leaflet-control"); 325 | this._container = container; 326 | this._map = map; 327 | this._layer = this.options.layer || new LayerGroup(); 328 | this._layer.addTo(map); 329 | this._event = undefined; 330 | this._compassHeading = null; 331 | this._prevBounds = null; 332 | 333 | const linkAndIcon = this.options.createButtonCallback(container, this.options); 334 | this._link = linkAndIcon.link; 335 | this._icon = linkAndIcon.icon; 336 | 337 | DomEvent.on( 338 | this._link, 339 | "click", 340 | function (ev) { 341 | DomEvent.stopPropagation(ev); 342 | DomEvent.preventDefault(ev); 343 | this._onClick(); 344 | }, 345 | this 346 | ).on(this._link, "dblclick", DomEvent.stopPropagation); 347 | 348 | this._resetVariables(); 349 | 350 | this._map.on("unload", this._unload, this); 351 | 352 | return container; 353 | }, 354 | 355 | /** 356 | * This method is called when the user clicks on the control. 357 | */ 358 | _onClick() { 359 | this._justClicked = true; 360 | const wasFollowing = this._isFollowing(); 361 | this._userPanned = false; 362 | this._userZoomed = false; 363 | 364 | if (this._active && !this._event) { 365 | // click while requesting 366 | this.stop(); 367 | } else if (this._active) { 368 | const behaviors = this.options.clickBehavior; 369 | let behavior = behaviors.outOfView; 370 | if (this._map.getBounds().contains(this._event.latlng)) { 371 | behavior = wasFollowing ? behaviors.inView : behaviors.inViewNotFollowing; 372 | } 373 | 374 | // Allow inheriting from another behavior 375 | if (behaviors[behavior]) { 376 | behavior = behaviors[behavior]; 377 | } 378 | 379 | switch (behavior) { 380 | case "setView": 381 | this.setView(); 382 | break; 383 | case "stop": 384 | this.stop(); 385 | if (this.options.returnToPrevBounds) { 386 | const f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds; 387 | f.bind(this._map)(this._prevBounds); 388 | } 389 | break; 390 | } 391 | } else { 392 | if (this.options.returnToPrevBounds) { 393 | this._prevBounds = this._map.getBounds(); 394 | } 395 | this.start(); 396 | } 397 | 398 | this._updateContainerStyle(); 399 | }, 400 | 401 | /** 402 | * Starts the plugin: 403 | * - activates the engine 404 | * - draws the marker (if coordinates available) 405 | */ 406 | start() { 407 | this._activate(); 408 | 409 | if (this._event) { 410 | this._drawMarker(this._map); 411 | 412 | // if we already have a location but the user clicked on the control 413 | if (this.options.setView) { 414 | this.setView(); 415 | } 416 | } 417 | this._updateContainerStyle(); 418 | }, 419 | 420 | /** 421 | * Stops the plugin: 422 | * - deactivates the engine 423 | * - reinitializes the button 424 | * - removes the marker 425 | */ 426 | stop() { 427 | this._deactivate(); 428 | 429 | this._cleanClasses(); 430 | this._resetVariables(); 431 | 432 | this._removeMarker(); 433 | }, 434 | 435 | /** 436 | * Keep the control active but stop following the location 437 | */ 438 | stopFollowing() { 439 | this._userPanned = true; 440 | this._updateContainerStyle(); 441 | this._drawMarker(); 442 | }, 443 | 444 | /** 445 | * This method launches the location engine. 446 | * It is called before the marker is updated, 447 | * event if it does not mean that the event will be ready. 448 | * 449 | * Override it if you want to add more functionalities. 450 | * It should set the this._active to true and do nothing if 451 | * this._active is true. 452 | */ 453 | _activate() { 454 | if (this._active || !this._map) { 455 | return; 456 | } 457 | 458 | this._map.locate(this.options.locateOptions); 459 | this._map.fire("locateactivate", this); 460 | this._active = true; 461 | 462 | // bind event listeners 463 | this._map.on("locationfound", this._onLocationFound, this); 464 | this._map.on("locationerror", this._onLocationError, this); 465 | this._map.on("dragstart", this._onDrag, this); 466 | this._map.on("zoomstart", this._onZoom, this); 467 | this._map.on("zoomend", this._onZoomEnd, this); 468 | if (this.options.showCompass) { 469 | const oriAbs = "ondeviceorientationabsolute" in window; 470 | if (oriAbs || "ondeviceorientation" in window) { 471 | const _this = this; 472 | const deviceorientation = function () { 473 | DomEvent.on(window, oriAbs ? "deviceorientationabsolute" : "deviceorientation", _this._onDeviceOrientation, _this); 474 | }; 475 | if (DeviceOrientationEvent && typeof DeviceOrientationEvent.requestPermission === "function") { 476 | DeviceOrientationEvent.requestPermission().then(function (permissionState) { 477 | if (permissionState === "granted") { 478 | deviceorientation(); 479 | } 480 | }); 481 | } else { 482 | deviceorientation(); 483 | } 484 | } 485 | } 486 | }, 487 | 488 | /** 489 | * Called to stop the location engine. 490 | * 491 | * Override it to shutdown any functionalities you added on start. 492 | */ 493 | _deactivate() { 494 | if (!this._active || !this._map) { 495 | return; 496 | } 497 | 498 | this._map.stopLocate(); 499 | this._map.fire("locatedeactivate", this); 500 | this._active = false; 501 | 502 | if (!this.options.cacheLocation) { 503 | this._event = undefined; 504 | } 505 | 506 | // unbind event listeners 507 | this._map.off("locationfound", this._onLocationFound, this); 508 | this._map.off("locationerror", this._onLocationError, this); 509 | this._map.off("dragstart", this._onDrag, this); 510 | this._map.off("zoomstart", this._onZoom, this); 511 | this._map.off("zoomend", this._onZoomEnd, this); 512 | if (this.options.showCompass) { 513 | this._compassHeading = null; 514 | if ("ondeviceorientationabsolute" in window) { 515 | DomEvent.off(window, "deviceorientationabsolute", this._onDeviceOrientation, this); 516 | } else if ("ondeviceorientation" in window) { 517 | DomEvent.off(window, "deviceorientation", this._onDeviceOrientation, this); 518 | } 519 | } 520 | }, 521 | 522 | /** 523 | * Zoom (unless we should keep the zoom level) and an to the current view. 524 | */ 525 | setView() { 526 | this._drawMarker(); 527 | if (this._isOutsideMapBounds()) { 528 | this._event = undefined; // clear the current location so we can get back into the bounds 529 | this.options.onLocationOutsideMapBounds(this); 530 | } else { 531 | if (this._justClicked && this.options.initialZoomLevel !== false) { 532 | let f = this.options.flyTo ? this._map.flyTo : this._map.setView; 533 | f.bind(this._map)([this._event.latitude, this._event.longitude], this.options.initialZoomLevel); 534 | } else if (this.options.keepCurrentZoomLevel) { 535 | let f = this.options.flyTo ? this._map.flyTo : this._map.panTo; 536 | f.bind(this._map)([this._event.latitude, this._event.longitude]); 537 | } else { 538 | let f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds; 539 | // Ignore zoom events while setting the viewport as these would stop following 540 | this._ignoreEvent = true; 541 | f.bind(this._map)(this.options.getLocationBounds(this._event), { 542 | padding: this.options.circlePadding, 543 | maxZoom: this.options.initialZoomLevel || this.options.locateOptions.maxZoom 544 | }); 545 | LeafletUtil.requestAnimFrame(function () { 546 | // Wait until after the next animFrame because the flyTo can be async 547 | this._ignoreEvent = false; 548 | }, this); 549 | } 550 | } 551 | }, 552 | 553 | /** 554 | * 555 | */ 556 | _drawCompass() { 557 | if (!this._event) { 558 | return; 559 | } 560 | 561 | const latlng = this._event.latlng; 562 | 563 | if (this.options.showCompass && latlng && this._compassHeading !== null) { 564 | const cStyle = this._isFollowing() ? this.options.followCompassStyle : this.options.compassStyle; 565 | if (!this._compass) { 566 | this._compass = new this.options.compassClass(latlng, this._compassHeading, cStyle).addTo(this._layer); 567 | } else { 568 | this._compass.setLatLng(latlng); 569 | this._compass.setHeading(this._compassHeading); 570 | // If the compassClass can be updated with setStyle, update it. 571 | if (this._compass.setStyle) { 572 | this._compass.setStyle(cStyle); 573 | } 574 | } 575 | // 576 | } 577 | if (this._compass && (!this.options.showCompass || this._compassHeading === null)) { 578 | this._compass.removeFrom(this._layer); 579 | this._compass = null; 580 | } 581 | }, 582 | 583 | /** 584 | * Draw the marker and accuracy circle on the map. 585 | * 586 | * Uses the event retrieved from onLocationFound from the map. 587 | */ 588 | _drawMarker() { 589 | if (this._event.accuracy === undefined) { 590 | this._event.accuracy = 0; 591 | } 592 | 593 | const radius = this._event.accuracy; 594 | const latlng = this._event.latlng; 595 | 596 | // circle with the radius of the location's accuracy 597 | if (this.options.drawCircle) { 598 | const style = this._isFollowing() ? this.options.followCircleStyle : this.options.circleStyle; 599 | 600 | if (!this._circle) { 601 | this._circle = circle(latlng, radius, style).addTo(this._layer); 602 | } else { 603 | this._circle.setLatLng(latlng).setRadius(radius).setStyle(style); 604 | } 605 | } 606 | 607 | let distance; 608 | let unit; 609 | if (this.options.metric) { 610 | distance = radius.toFixed(0); 611 | unit = this.options.strings.metersUnit; 612 | } else { 613 | distance = (radius * 3.2808399).toFixed(0); 614 | unit = this.options.strings.feetUnit; 615 | } 616 | 617 | // small inner marker 618 | if (this.options.drawMarker) { 619 | const mStyle = this._isFollowing() ? this.options.followMarkerStyle : this.options.markerStyle; 620 | if (!this._marker) { 621 | this._marker = new this.options.markerClass(latlng, mStyle).addTo(this._layer); 622 | } else { 623 | this._marker.setLatLng(latlng); 624 | // If the markerClass can be updated with setStyle, update it. 625 | if (this._marker.setStyle) { 626 | this._marker.setStyle(mStyle); 627 | } 628 | } 629 | } 630 | 631 | this._drawCompass(); 632 | 633 | const t = this.options.strings.popup; 634 | function getPopupText() { 635 | if (typeof t === "string") { 636 | return LeafletUtil.template(t, { distance, unit }); 637 | } else if (typeof t === "function") { 638 | return t({ distance, unit }); 639 | } else { 640 | return t; 641 | } 642 | } 643 | if (this.options.showPopup && t && this._marker) { 644 | this._marker.bindPopup(getPopupText())._popup.setLatLng(latlng); 645 | } 646 | if (this.options.showPopup && t && this._compass) { 647 | this._compass.bindPopup(getPopupText())._popup.setLatLng(latlng); 648 | } 649 | }, 650 | 651 | /** 652 | * Remove the marker from map. 653 | */ 654 | _removeMarker() { 655 | this._layer.clearLayers(); 656 | this._marker = undefined; 657 | this._circle = undefined; 658 | }, 659 | 660 | /** 661 | * Unload the plugin and all event listeners. 662 | * Kind of the opposite of onAdd. 663 | */ 664 | _unload() { 665 | this.stop(); 666 | // May become undefined during HMR 667 | if (this._map) { 668 | this._map.off("unload", this._unload, this); 669 | } 670 | }, 671 | 672 | /** 673 | * Sets the compass heading 674 | */ 675 | _setCompassHeading(angle) { 676 | if (!isNaN(parseFloat(angle)) && isFinite(angle)) { 677 | angle = Math.round(angle); 678 | 679 | this._compassHeading = angle; 680 | LeafletUtil.requestAnimFrame(this._drawCompass, this); 681 | } else { 682 | this._compassHeading = null; 683 | } 684 | }, 685 | 686 | /** 687 | * If the compass fails calibration just fail safely and remove the compass 688 | */ 689 | _onCompassNeedsCalibration() { 690 | this._setCompassHeading(); 691 | }, 692 | 693 | /** 694 | * Process and normalise compass events 695 | */ 696 | _onDeviceOrientation(e) { 697 | if (!this._active) { 698 | return; 699 | } 700 | 701 | if (e.webkitCompassHeading) { 702 | // iOS 703 | this._setCompassHeading(e.webkitCompassHeading); 704 | } else if (e.absolute && e.alpha) { 705 | // Android 706 | this._setCompassHeading(360 - e.alpha); 707 | } 708 | }, 709 | 710 | /** 711 | * Calls deactivate and dispatches an error. 712 | */ 713 | _onLocationError(err) { 714 | // ignore time out error if the location is watched 715 | if (err.code == 3 && this.options.locateOptions.watch) { 716 | return; 717 | } 718 | 719 | this.stop(); 720 | this.options.onLocationError(err, this); 721 | }, 722 | 723 | /** 724 | * Stores the received event and updates the marker. 725 | */ 726 | _onLocationFound(e) { 727 | // no need to do anything if the location has not changed 728 | if (this._event && this._event.latlng.lat === e.latlng.lat && this._event.latlng.lng === e.latlng.lng && this._event.accuracy === e.accuracy) { 729 | return; 730 | } 731 | 732 | if (!this._active) { 733 | // we may have a stray event 734 | return; 735 | } 736 | 737 | this._event = e; 738 | 739 | this._drawMarker(); 740 | this._updateContainerStyle(); 741 | 742 | switch (this.options.setView) { 743 | case "once": 744 | if (this._justClicked) { 745 | this.setView(); 746 | } 747 | break; 748 | case "untilPan": 749 | if (!this._userPanned) { 750 | this.setView(); 751 | } 752 | break; 753 | case "untilPanOrZoom": 754 | if (!this._userPanned && !this._userZoomed) { 755 | this.setView(); 756 | } 757 | break; 758 | case "always": 759 | this.setView(); 760 | break; 761 | case false: 762 | // don't set the view 763 | break; 764 | } 765 | 766 | this._justClicked = false; 767 | }, 768 | 769 | /** 770 | * When the user drags. Need a separate event so we can bind and unbind event listeners. 771 | */ 772 | _onDrag() { 773 | // only react to drags once we have a location 774 | if (this._event && !this._ignoreEvent) { 775 | this._userPanned = true; 776 | this._updateContainerStyle(); 777 | this._drawMarker(); 778 | } 779 | }, 780 | 781 | /** 782 | * When the user zooms. Need a separate event so we can bind and unbind event listeners. 783 | */ 784 | _onZoom() { 785 | // only react to drags once we have a location 786 | if (this._event && !this._ignoreEvent) { 787 | this._userZoomed = true; 788 | this._updateContainerStyle(); 789 | this._drawMarker(); 790 | } 791 | }, 792 | 793 | /** 794 | * After a zoom ends update the compass and handle sideways zooms 795 | */ 796 | _onZoomEnd() { 797 | if (this._event) { 798 | this._drawCompass(); 799 | } 800 | 801 | if (this._event && !this._ignoreEvent) { 802 | // If we have zoomed in and out and ended up sideways treat it as a pan 803 | if (this._marker && !this._map.getBounds().pad(-0.3).contains(this._marker.getLatLng())) { 804 | this._userPanned = true; 805 | this._updateContainerStyle(); 806 | this._drawMarker(); 807 | } 808 | } 809 | }, 810 | 811 | /** 812 | * Compute whether the map is following the user location with pan and zoom. 813 | */ 814 | _isFollowing() { 815 | if (!this._active) { 816 | return false; 817 | } 818 | 819 | if (this.options.setView === "always") { 820 | return true; 821 | } else if (this.options.setView === "untilPan") { 822 | return !this._userPanned; 823 | } else if (this.options.setView === "untilPanOrZoom") { 824 | return !this._userPanned && !this._userZoomed; 825 | } 826 | }, 827 | 828 | /** 829 | * Check if location is in map bounds 830 | */ 831 | _isOutsideMapBounds() { 832 | if (this._event === undefined) { 833 | return false; 834 | } 835 | return this._map.options.maxBounds && !this._map.options.maxBounds.contains(this._event.latlng); 836 | }, 837 | 838 | /** 839 | * Toggles button class between following and active. 840 | */ 841 | _updateContainerStyle() { 842 | if (!this._container) { 843 | return; 844 | } 845 | 846 | if (this._active && !this._event) { 847 | // active but don't have a location yet 848 | this._setClasses("requesting"); 849 | } else if (this._isFollowing()) { 850 | this._setClasses("following"); 851 | } else if (this._active) { 852 | this._setClasses("active"); 853 | } else { 854 | this._cleanClasses(); 855 | } 856 | }, 857 | 858 | /** 859 | * Sets the CSS classes for the state. 860 | */ 861 | _setClasses(state) { 862 | if (state == "requesting") { 863 | removeClasses(this._container, "active following"); 864 | addClasses(this._container, "requesting"); 865 | 866 | removeClasses(this._icon, this.options.icon); 867 | addClasses(this._icon, this.options.iconLoading); 868 | } else if (state == "active") { 869 | removeClasses(this._container, "requesting following"); 870 | addClasses(this._container, "active"); 871 | 872 | removeClasses(this._icon, this.options.iconLoading); 873 | addClasses(this._icon, this.options.icon); 874 | } else if (state == "following") { 875 | removeClasses(this._container, "requesting"); 876 | addClasses(this._container, "active following"); 877 | 878 | removeClasses(this._icon, this.options.iconLoading); 879 | addClasses(this._icon, this.options.icon); 880 | } 881 | }, 882 | 883 | /** 884 | * Removes all classes from button. 885 | */ 886 | _cleanClasses() { 887 | DomUtil.removeClass(this._container, "requesting"); 888 | DomUtil.removeClass(this._container, "active"); 889 | DomUtil.removeClass(this._container, "following"); 890 | 891 | removeClasses(this._icon, this.options.iconLoading); 892 | addClasses(this._icon, this.options.icon); 893 | }, 894 | 895 | /** 896 | * Reinitializes state variables. 897 | */ 898 | _resetVariables() { 899 | // whether locate is active or not 900 | this._active = false; 901 | 902 | // true if the control was clicked for the first time 903 | // we need this so we can pan and zoom once we have the location 904 | this._justClicked = false; 905 | 906 | // true if the user has panned the map after clicking the control 907 | this._userPanned = false; 908 | 909 | // true if the user has zoomed the map after clicking the control 910 | this._userZoomed = false; 911 | } 912 | }); 913 | 914 | function locate(options) { 915 | return new LocateControl(options); 916 | } 917 | 918 | export { LocationMarker, CompassMarker, LocateControl, locate }; 919 | -------------------------------------------------------------------------------- /src/L.Control.Locate.mapbox.scss: -------------------------------------------------------------------------------- 1 | @use "L.Control.Locate"; 2 | 3 | /* Mapbox specific adjustments */ 4 | 5 | .leaflet-control-locate a .leaflet-control-locate-location-arrow, 6 | .leaflet-control-locate a .leaflet-control-locate-spinner { 7 | margin: 5px; 8 | } 9 | -------------------------------------------------------------------------------- /src/L.Control.Locate.scss: -------------------------------------------------------------------------------- 1 | @function svg-icon-arrow($color) { 2 | @return url('data:image/svg+xml;charset=UTF-8,'); 3 | } 4 | 5 | @function svg-icon-spinner($color) { 6 | @return url('data:image/svg+xml;charset=UTF-8,'); 7 | } 8 | 9 | .leaflet-control-locate { 10 | a { 11 | cursor: pointer; 12 | 13 | .leaflet-control-locate-location-arrow { 14 | display: inline-block; 15 | width: 16px; 16 | height: 16px; 17 | margin: 7px; 18 | background-image: svg-icon-arrow(black); 19 | } 20 | 21 | .leaflet-control-locate-spinner { 22 | display: inline-block; 23 | width: 16px; 24 | height: 16px; 25 | margin: 7px; 26 | background-image: svg-icon-spinner(black); 27 | animation: leaflet-control-locate-spin 2s linear infinite; 28 | } 29 | } 30 | 31 | &.active a .leaflet-control-locate-location-arrow { 32 | background-image: svg-icon-arrow(rgb(32, 116, 182)); 33 | } 34 | 35 | &.following a .leaflet-control-locate-location-arrow { 36 | background-image: svg-icon-arrow(rgb(252, 132, 40)); 37 | } 38 | } 39 | 40 | .leaflet-touch .leaflet-bar .leaflet-locate-text-active { 41 | width: 100%; 42 | max-width: 200px; 43 | text-overflow: ellipsis; 44 | white-space: nowrap; 45 | overflow: hidden; 46 | padding: 0 10px; 47 | 48 | .leaflet-locate-icon { 49 | padding: 0 5px 0 0; 50 | } 51 | } 52 | 53 | .leaflet-control-locate-location circle { 54 | animation: leaflet-control-locate-throb 4s ease infinite; 55 | } 56 | 57 | @keyframes leaflet-control-locate-throb { 58 | 0% { 59 | stroke-width: 1; 60 | } 61 | 62 | 50% { 63 | stroke-width: 3; 64 | transform: scale(0.8, 0.8); 65 | } 66 | 67 | 100% { 68 | stroke-width: 1; 69 | } 70 | } 71 | 72 | @keyframes leaflet-control-locate-spin { 73 | 0% { 74 | transform: rotate(0deg); 75 | } 76 | 77 | 100% { 78 | transform: rotate(360deg); 79 | } 80 | } 81 | --------------------------------------------------------------------------------