├── .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 | [](http://badge.fury.io/js/leaflet.locatecontrol)
4 | [](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 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------