├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .npmignore
├── screenshot.png
├── .stylelintrc.json
├── src
├── L.Control.Locate.mapbox.css
├── L.Control.Locate.css
├── L.Control.Locate.d.ts
└── L.Control.Locate.js
├── eslint.config.js
├── demo_mapbox
├── script.js
└── index.html
├── demo
├── index.html
├── script.js
└── style.css
├── LICENSE
├── CHANGELOG.md
├── demo_esm
├── index.html
├── script.js
└── style.css
├── scripts
└── server.js
├── rollup.config.js
├── dist
├── L.Control.Locate.min.css.map
├── L.Control.Locate.css.map
├── L.Control.Locate.d.ts
├── L.Control.Locate.mapbox.min.css.map
├── L.Control.Locate.mapbox.css.map
├── L.Control.Locate.min.css
├── L.Control.Locate.mapbox.min.css
├── L.Control.Locate.css
├── L.Control.Locate.mapbox.css
├── L.Control.Locate.min.js
├── L.Control.Locate.esm.js
└── L.Control.Locate.umd.js
├── package.json
├── index.html
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .sass-cache
2 | node_modules/*
3 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore artifacts:
2 | dist/*
3 | node_modules/*
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 160,
3 | "trailingComma": "none"
4 | }
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .sass-cache
2 | demo_mapbox/*
3 | demo/*
4 | node_modules/*
5 | screenshot.png
6 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/domoritz/leaflet-locatecontrol/HEAD/screenshot.png
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["stylelint-prettier"],
3 | "root": true,
4 | "rules": {
5 | "prettier/prettier": true
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/L.Control.Locate.mapbox.css:
--------------------------------------------------------------------------------
1 | @import "L.Control.Locate.css";
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 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
2 | import globals from "globals";
3 |
4 | export default [
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
23 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
27 |
28 |
--------------------------------------------------------------------------------
/scripts/server.js:
--------------------------------------------------------------------------------
1 | import http from "node:http";
2 | import fs from "node:fs";
3 | import path from "node:path";
4 |
5 | const PORT = 9000;
6 | const ROOT = process.cwd();
7 |
8 | const MIME_TYPES = {
9 | ".html": "text/html",
10 | ".js": "text/javascript",
11 | ".css": "text/css",
12 | ".json": "application/json",
13 | ".png": "image/png",
14 | ".ico": "image/x-icon"
15 | };
16 |
17 | const server = http.createServer((req, res) => {
18 | console.log(`${req.method} ${req.url}`);
19 |
20 | let filePath = path.join(ROOT, req.url.split("?")[0]);
21 |
22 | fs.stat(filePath, (err, stats) => {
23 | if (err) {
24 | res.writeHead(404, { "Content-Type": "text/plain" });
25 | res.end("404 Not Found");
26 | return;
27 | }
28 |
29 | // If it is a directory, look for index.html
30 | if (stats.isDirectory()) {
31 | filePath = path.join(filePath, "index.html");
32 | }
33 |
34 | fs.readFile(filePath, (err, data) => {
35 | if (err) {
36 | res.writeHead(404, { "Content-Type": "text/plain" });
37 | res.end("404 Not Found");
38 | return;
39 | }
40 |
41 | const ext = path.extname(filePath).toLowerCase();
42 | const contentType = MIME_TYPES[ext] || "application/octet-stream";
43 |
44 | res.writeHead(200, { "Content-Type": contentType });
45 | res.end(data);
46 | });
47 | });
48 | });
49 |
50 | server.listen(PORT, "localhost", () => {
51 | console.log(`Server running at http://localhost:${PORT}/`);
52 | });
53 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { nodeResolve } from "@rollup/plugin-node-resolve";
2 | import commonjs from "@rollup/plugin-commonjs";
3 | import terser from "@rollup/plugin-terser";
4 | import { readFileSync } from "fs";
5 |
6 | const pkg = JSON.parse(readFileSync("./package.json", "utf-8"));
7 | const banner = `/*! Version: ${pkg.version}\nCopyright (c) 2016 Dominik Moritz */\n`;
8 |
9 | const footer = `
10 | (function() {
11 | if (typeof window !== 'undefined' && window.L) {
12 | window.L.control = window.L.control || {};
13 | window.L.control.locate = window.L.Control.Locate.locate;
14 | }
15 | })();
16 | `;
17 |
18 | export default [
19 | // ESM build
20 | {
21 | input: "src/L.Control.Locate.js",
22 | external: ["leaflet"],
23 | output: {
24 | file: "dist/L.Control.Locate.esm.js",
25 | format: "es"
26 | },
27 | plugins: [nodeResolve(), commonjs()]
28 | },
29 | // UMD build
30 | {
31 | input: "src/L.Control.Locate.js",
32 | external: ["leaflet"],
33 | output: {
34 | file: "dist/L.Control.Locate.umd.js",
35 | format: "umd",
36 | name: "L.Control.Locate",
37 | globals: {
38 | leaflet: "L"
39 | },
40 | esModule: true,
41 | footer: footer
42 | },
43 | plugins: [nodeResolve(), commonjs()]
44 | },
45 | // Minified UMD build
46 | {
47 | input: "src/L.Control.Locate.js",
48 | external: ["leaflet"],
49 | output: {
50 | file: "dist/L.Control.Locate.min.js",
51 | format: "umd",
52 | name: "L.Control.Locate",
53 | globals: {
54 | leaflet: "L"
55 | },
56 | esModule: true,
57 | banner: banner,
58 | footer: footer,
59 | sourcemap: true
60 | },
61 | plugins: [
62 | nodeResolve(),
63 | commonjs(),
64 | terser({
65 | format: {
66 | comments: false,
67 | preamble: banner
68 | }
69 | })
70 | ]
71 | }
72 | ];
73 |
--------------------------------------------------------------------------------
/demo_esm/script.js:
--------------------------------------------------------------------------------
1 | import { Map, TileLayer } from "leaflet";
2 | import { LocateControl } from "../dist/L.Control.Locate.esm.js";
3 |
4 | // Dark Mode Toggle
5 | const darkModeToggle = document.getElementById("dark-mode-toggle");
6 | const root = document.documentElement;
7 | const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
8 |
9 | // Check for saved preference
10 | const savedTheme = localStorage.getItem("theme");
11 | if (savedTheme) {
12 | root.setAttribute("data-theme", savedTheme);
13 | }
14 |
15 | // Helper to get current effective theme
16 | function getCurrentTheme() {
17 | const dataTheme = root.getAttribute("data-theme");
18 | if (dataTheme) return dataTheme;
19 | return prefersDark ? "dark" : "light";
20 | }
21 |
22 | const osmUrl = "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
23 | const osmAttrib = 'Map data © OpenStreetMap contributors';
24 | let osm = new TileLayer(osmUrl, {
25 | attribution: osmAttrib,
26 | detectRetina: true
27 | });
28 |
29 | const osmDarkUrl = "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png";
30 | const osmDarkAttrib =
31 | '© OpenStreetMap contributors © CARTO ';
32 | let osmDark = new TileLayer(osmDarkUrl, {
33 | attribution: osmDarkAttrib,
34 | detectRetina: true
35 | });
36 |
37 | function updateMapLayer() {
38 | if (getCurrentTheme() === "dark") {
39 | map.removeLayer(osm);
40 | osmDark.addTo(map);
41 | } else {
42 | map.removeLayer(osmDark);
43 | osm.addTo(map);
44 | }
45 | }
46 |
47 | darkModeToggle.addEventListener("click", () => {
48 | const currentTheme = getCurrentTheme();
49 | const newTheme = currentTheme === "dark" ? "light" : "dark";
50 | root.setAttribute("data-theme", newTheme);
51 | localStorage.setItem("theme", newTheme);
52 | updateMapLayer();
53 | });
54 |
55 | let map = new Map("map", {
56 | layers: [getCurrentTheme() === "dark" ? osmDark : osm],
57 | center: [51.505, -0.09],
58 | zoom: 10,
59 | zoomControl: true
60 | });
61 |
62 | let lc = new LocateControl({
63 | strings: {
64 | title: "Show me where I am, yo!"
65 | }
66 | }).addTo(map);
67 |
--------------------------------------------------------------------------------
/dist/L.Control.Locate.min.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"mappings":"AAAA,yIAMI,0QAAA,mQAWA,mgBAIA,29BAMF,4HAIA,kIAKF,4JAQE,+FAKF,2FAIA,0HAeA","sources":["src/L.Control.Locate.css"],"sourcesContent":[".leaflet-control-locate {\n --locate-control-icon-color: black;\n --locate-control-active-color: rgb(32, 116, 182);\n --locate-control-following-color: rgb(252, 132, 40);\n\n a {\n .leaflet-control-locate-location-arrow,\n .leaflet-control-locate-spinner {\n display: inline-block;\n width: 16px;\n height: 16px;\n margin: 7px;\n background-color: var(--locate-control-icon-color);\n mask-repeat: no-repeat;\n mask-position: center;\n }\n\n .leaflet-control-locate-location-arrow {\n mask-image: url('data:image/svg+xml;charset=UTF-8, ');\n }\n\n .leaflet-control-locate-spinner {\n mask-image: url('data:image/svg+xml;charset=UTF-8, ');\n animation: leaflet-control-locate-spin 2s linear infinite;\n }\n }\n\n &.active a .leaflet-control-locate-location-arrow {\n background-color: var(--locate-control-active-color);\n }\n\n &.following a .leaflet-control-locate-location-arrow {\n background-color: var(--locate-control-following-color);\n }\n}\n\n.leaflet-touch .leaflet-bar .leaflet-locate-text-active {\n width: 100%;\n max-width: 200px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n padding: 0 10px;\n\n .leaflet-locate-icon {\n padding: 0 5px 0 0;\n }\n}\n\n.leaflet-control-locate-location circle {\n animation: leaflet-control-locate-throb 4s ease infinite;\n}\n\n@keyframes leaflet-control-locate-throb {\n 0% {\n stroke-width: 1px;\n }\n\n 50% {\n stroke-width: 3px;\n transform: scale(0.8, 0.8);\n }\n\n 100% {\n stroke-width: 1px;\n }\n}\n\n@keyframes leaflet-control-locate-spin {\n 0% {\n transform: rotate(0deg);\n }\n\n 100% {\n transform: rotate(360deg);\n }\n}\n"],"names":[]}
--------------------------------------------------------------------------------
/dist/L.Control.Locate.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"mappings":"AAAA;;;;;;AAMI;;;;;;;;;;;;AAAA;;;;;;;;;;;;AAWA;;;;;AAIA;;;;;;AAMF;;;;AAIA;;;;AAKF;;;;;;;;;AAQE;;;;AAKF;;;;AAIA;;;;;;;;;;;;;;;AAeA","sources":["src/L.Control.Locate.css"],"sourcesContent":[".leaflet-control-locate {\n --locate-control-icon-color: black;\n --locate-control-active-color: rgb(32, 116, 182);\n --locate-control-following-color: rgb(252, 132, 40);\n\n a {\n .leaflet-control-locate-location-arrow,\n .leaflet-control-locate-spinner {\n display: inline-block;\n width: 16px;\n height: 16px;\n margin: 7px;\n background-color: var(--locate-control-icon-color);\n mask-repeat: no-repeat;\n mask-position: center;\n }\n\n .leaflet-control-locate-location-arrow {\n mask-image: url('data:image/svg+xml;charset=UTF-8, ');\n }\n\n .leaflet-control-locate-spinner {\n mask-image: url('data:image/svg+xml;charset=UTF-8, ');\n animation: leaflet-control-locate-spin 2s linear infinite;\n }\n }\n\n &.active a .leaflet-control-locate-location-arrow {\n background-color: var(--locate-control-active-color);\n }\n\n &.following a .leaflet-control-locate-location-arrow {\n background-color: var(--locate-control-following-color);\n }\n}\n\n.leaflet-touch .leaflet-bar .leaflet-locate-text-active {\n width: 100%;\n max-width: 200px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n padding: 0 10px;\n\n .leaflet-locate-icon {\n padding: 0 5px 0 0;\n }\n}\n\n.leaflet-control-locate-location circle {\n animation: leaflet-control-locate-throb 4s ease infinite;\n}\n\n@keyframes leaflet-control-locate-throb {\n 0% {\n stroke-width: 1px;\n }\n\n 50% {\n stroke-width: 3px;\n transform: scale(0.8, 0.8);\n }\n\n 100% {\n stroke-width: 1px;\n }\n}\n\n@keyframes leaflet-control-locate-spin {\n 0% {\n transform: rotate(0deg);\n }\n\n 100% {\n transform: rotate(360deg);\n }\n}\n"],"names":[]}
--------------------------------------------------------------------------------
/src/L.Control.Locate.css:
--------------------------------------------------------------------------------
1 | .leaflet-control-locate {
2 | --locate-control-icon-color: black;
3 | --locate-control-active-color: rgb(32, 116, 182);
4 | --locate-control-following-color: rgb(252, 132, 40);
5 |
6 | a {
7 | .leaflet-control-locate-location-arrow,
8 | .leaflet-control-locate-spinner {
9 | display: inline-block;
10 | width: 16px;
11 | height: 16px;
12 | margin: 7px;
13 | background-color: var(--locate-control-icon-color);
14 | mask-repeat: no-repeat;
15 | mask-position: center;
16 | }
17 |
18 | .leaflet-control-locate-location-arrow {
19 | mask-image: url('data:image/svg+xml;charset=UTF-8, ');
20 | }
21 |
22 | .leaflet-control-locate-spinner {
23 | mask-image: url('data:image/svg+xml;charset=UTF-8, ');
24 | animation: leaflet-control-locate-spin 2s linear infinite;
25 | }
26 | }
27 |
28 | &.active a .leaflet-control-locate-location-arrow {
29 | background-color: var(--locate-control-active-color);
30 | }
31 |
32 | &.following a .leaflet-control-locate-location-arrow {
33 | background-color: var(--locate-control-following-color);
34 | }
35 | }
36 |
37 | .leaflet-touch .leaflet-bar .leaflet-locate-text-active {
38 | width: 100%;
39 | max-width: 200px;
40 | text-overflow: ellipsis;
41 | white-space: nowrap;
42 | overflow: hidden;
43 | padding: 0 10px;
44 |
45 | .leaflet-locate-icon {
46 | padding: 0 5px 0 0;
47 | }
48 | }
49 |
50 | .leaflet-control-locate-location circle {
51 | animation: leaflet-control-locate-throb 4s ease infinite;
52 | }
53 |
54 | @keyframes leaflet-control-locate-throb {
55 | 0% {
56 | stroke-width: 1px;
57 | }
58 |
59 | 50% {
60 | stroke-width: 3px;
61 | transform: scale(0.8, 0.8);
62 | }
63 |
64 | 100% {
65 | stroke-width: 1px;
66 | }
67 | }
68 |
69 | @keyframes leaflet-control-locate-spin {
70 | 0% {
71 | transform: rotate(0deg);
72 | }
73 |
74 | 100% {
75 | transform: rotate(360deg);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "leaflet.locatecontrol",
3 | "version": "0.85.1",
4 | "type": "module",
5 | "homepage": "https://github.com/domoritz/leaflet-locatecontrol",
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.min.js",
8 | "unpkg": "dist/L.Control.Locate.min.js",
9 | "jsdelivr": "dist/L.Control.Locate.min.js",
10 | "module": "dist/L.Control.Locate.esm.js",
11 | "types": "dist/L.Control.Locate.d.ts",
12 | "author": "Dominik Moritz ",
13 | "repository": {
14 | "type": "git",
15 | "url": "git@github.com:domoritz/leaflet-locatecontrol.git"
16 | },
17 | "keywords": [
18 | "leaflet",
19 | "locate",
20 | "plugin"
21 | ],
22 | "license": "MIT",
23 | "readmeFilename": "README.md",
24 | "scripts": {
25 | "build": "npm run build:js && npm run build:css && npm run build:types",
26 | "build:js": "rollup -c",
27 | "build:css": "npm run build:css:leaflet && npm run build:css:mapbox",
28 | "build:css:leaflet": "lightningcss --bundle --sourcemap --targets '>= 0.25%' src/L.Control.Locate.css -o dist/L.Control.Locate.css && lightningcss --minify --bundle --sourcemap --targets '>= 0.25%' src/L.Control.Locate.css -o dist/L.Control.Locate.min.css",
29 | "build:css:mapbox": "lightningcss --bundle --sourcemap --targets '>= 0.25%' src/L.Control.Locate.mapbox.css -o dist/L.Control.Locate.mapbox.css && lightningcss --minify --bundle --sourcemap --targets '>= 0.25%' src/L.Control.Locate.mapbox.css -o dist/L.Control.Locate.mapbox.min.css",
30 | "build:types": "cp src/L.Control.Locate.d.ts dist/L.Control.Locate.d.ts",
31 | "bump:minor": "npm version minor",
32 | "bump:patch": "npm version patch",
33 | "version": "npm run build && git add -A dist",
34 | "lint": "eslint && stylelint **/*.css && prettier --check .",
35 | "lint:fix": "eslint --fix && stylelint --fix **/*.css && prettier --write .",
36 | "start": "node scripts/server.js"
37 | },
38 | "devDependencies": {
39 | "@rollup/plugin-commonjs": "^29.0.0",
40 | "@rollup/plugin-node-resolve": "^16.0.3",
41 | "@rollup/plugin-terser": "^0.4.4",
42 | "eslint": "^9.23.0",
43 | "eslint-config-prettier": "^10.1.1",
44 | "eslint-plugin-prettier": "^5.2.5",
45 | "leaflet": "^1.9.4",
46 | "lightningcss-cli": "^1.30.2",
47 | "prettier": "^3.5.3",
48 | "rollup": "^4.53.3",
49 | "stylelint": "^16.17.0",
50 | "stylelint-prettier": "^5.0.3",
51 | "typescript": "^5.9.3"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/dist/L.Control.Locate.mapbox.min.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"mappings":"ACAA,yIAMI,0QAAA,mQAWA,mgBAIA,29BAMF,4HAIA,kIAKF,4JAQE,+FAKF,2FAIA,0HAeA,2FDhEA","sources":["src/L.Control.Locate.mapbox.css","src/L.Control.Locate.css"],"sourcesContent":["@import \"L.Control.Locate.css\";\n\n/* Mapbox specific adjustments */\n\n.leaflet-control-locate a .leaflet-control-locate-location-arrow,\n.leaflet-control-locate a .leaflet-control-locate-spinner {\n margin: 5px;\n}\n",".leaflet-control-locate {\n --locate-control-icon-color: black;\n --locate-control-active-color: rgb(32, 116, 182);\n --locate-control-following-color: rgb(252, 132, 40);\n\n a {\n .leaflet-control-locate-location-arrow,\n .leaflet-control-locate-spinner {\n display: inline-block;\n width: 16px;\n height: 16px;\n margin: 7px;\n background-color: var(--locate-control-icon-color);\n mask-repeat: no-repeat;\n mask-position: center;\n }\n\n .leaflet-control-locate-location-arrow {\n mask-image: url('data:image/svg+xml;charset=UTF-8, ');\n }\n\n .leaflet-control-locate-spinner {\n mask-image: url('data:image/svg+xml;charset=UTF-8, ');\n animation: leaflet-control-locate-spin 2s linear infinite;\n }\n }\n\n &.active a .leaflet-control-locate-location-arrow {\n background-color: var(--locate-control-active-color);\n }\n\n &.following a .leaflet-control-locate-location-arrow {\n background-color: var(--locate-control-following-color);\n }\n}\n\n.leaflet-touch .leaflet-bar .leaflet-locate-text-active {\n width: 100%;\n max-width: 200px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n padding: 0 10px;\n\n .leaflet-locate-icon {\n padding: 0 5px 0 0;\n }\n}\n\n.leaflet-control-locate-location circle {\n animation: leaflet-control-locate-throb 4s ease infinite;\n}\n\n@keyframes leaflet-control-locate-throb {\n 0% {\n stroke-width: 1px;\n }\n\n 50% {\n stroke-width: 3px;\n transform: scale(0.8, 0.8);\n }\n\n 100% {\n stroke-width: 1px;\n }\n}\n\n@keyframes leaflet-control-locate-spin {\n 0% {\n transform: rotate(0deg);\n }\n\n 100% {\n transform: rotate(360deg);\n }\n}\n"],"names":[]}
--------------------------------------------------------------------------------
/dist/L.Control.Locate.mapbox.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"mappings":"ACAA;;;;;;AAMI;;;;;;;;;;;;AAAA;;;;;;;;;;;;AAWA;;;;;AAIA;;;;;;AAMF;;;;AAIA;;;;AAKF;;;;;;;;;AAQE;;;;AAKF;;;;AAIA;;;;;;;;;;;;;;;AAeA;;;;;;;;;;ADhEA","sources":["src/L.Control.Locate.mapbox.css","src/L.Control.Locate.css"],"sourcesContent":["@import \"L.Control.Locate.css\";\n\n/* Mapbox specific adjustments */\n\n.leaflet-control-locate a .leaflet-control-locate-location-arrow,\n.leaflet-control-locate a .leaflet-control-locate-spinner {\n margin: 5px;\n}\n",".leaflet-control-locate {\n --locate-control-icon-color: black;\n --locate-control-active-color: rgb(32, 116, 182);\n --locate-control-following-color: rgb(252, 132, 40);\n\n a {\n .leaflet-control-locate-location-arrow,\n .leaflet-control-locate-spinner {\n display: inline-block;\n width: 16px;\n height: 16px;\n margin: 7px;\n background-color: var(--locate-control-icon-color);\n mask-repeat: no-repeat;\n mask-position: center;\n }\n\n .leaflet-control-locate-location-arrow {\n mask-image: url('data:image/svg+xml;charset=UTF-8, ');\n }\n\n .leaflet-control-locate-spinner {\n mask-image: url('data:image/svg+xml;charset=UTF-8, ');\n animation: leaflet-control-locate-spin 2s linear infinite;\n }\n }\n\n &.active a .leaflet-control-locate-location-arrow {\n background-color: var(--locate-control-active-color);\n }\n\n &.following a .leaflet-control-locate-location-arrow {\n background-color: var(--locate-control-following-color);\n }\n}\n\n.leaflet-touch .leaflet-bar .leaflet-locate-text-active {\n width: 100%;\n max-width: 200px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n padding: 0 10px;\n\n .leaflet-locate-icon {\n padding: 0 5px 0 0;\n }\n}\n\n.leaflet-control-locate-location circle {\n animation: leaflet-control-locate-throb 4s ease infinite;\n}\n\n@keyframes leaflet-control-locate-throb {\n 0% {\n stroke-width: 1px;\n }\n\n 50% {\n stroke-width: 3px;\n transform: scale(0.8, 0.8);\n }\n\n 100% {\n stroke-width: 1px;\n }\n}\n\n@keyframes leaflet-control-locate-spin {\n 0% {\n transform: rotate(0deg);\n }\n\n 100% {\n transform: rotate(360deg);\n }\n}\n"],"names":[]}
--------------------------------------------------------------------------------
/demo/script.js:
--------------------------------------------------------------------------------
1 | // Dark Mode Toggle
2 | const darkModeToggle = document.getElementById("dark-mode-toggle");
3 | const root = document.documentElement;
4 | const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
5 |
6 | // Check for saved preference
7 | const savedTheme = localStorage.getItem("theme");
8 | if (savedTheme) {
9 | root.setAttribute("data-theme", savedTheme);
10 | }
11 |
12 | // Helper to get current effective theme
13 | function getCurrentTheme() {
14 | const dataTheme = root.getAttribute("data-theme");
15 | if (dataTheme) return dataTheme;
16 | return prefersDark ? "dark" : "light";
17 | }
18 |
19 | const osmUrl = "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
20 | const osmAttrib = 'Map data © OpenStreetMap contributors';
21 | let osm = new L.TileLayer(osmUrl, {
22 | attribution: osmAttrib,
23 | detectRetina: true
24 | });
25 |
26 | const osmDarkUrl = "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png";
27 | const osmDarkAttrib =
28 | '© OpenStreetMap contributors © CARTO ';
29 | let osmDark = new L.TileLayer(osmDarkUrl, {
30 | attribution: osmDarkAttrib,
31 | detectRetina: true
32 | });
33 |
34 | function updateMapLayer() {
35 | if (getCurrentTheme() === "dark") {
36 | map.removeLayer(osm);
37 | map.removeLayer(mapbox);
38 | osmDark.addTo(map);
39 | } else {
40 | map.removeLayer(osmDark);
41 | osm.addTo(map);
42 | }
43 | }
44 |
45 | darkModeToggle.addEventListener("click", () => {
46 | const currentTheme = getCurrentTheme();
47 | const newTheme = currentTheme === "dark" ? "light" : "dark";
48 | root.setAttribute("data-theme", newTheme);
49 | localStorage.setItem("theme", newTheme);
50 | updateMapLayer();
51 | });
52 |
53 | // please replace this with your own mapbox token!
54 | const token = "pk.eyJ1IjoiZG9tb3JpdHoiLCJhIjoiY2s4a2d0OHp3MDFxMTNmcWoxdDVmdHF4MiJ9.y9-0BZCXJBpNBzEHxhFq1Q";
55 | const mapboxUrl = "https://api.mapbox.com/styles/v1/mapbox/streets-v10/tiles/{z}/{x}/{y}@2x?access_token=" + token;
56 | const mapboxAttrib = 'Map data © OpenStreetMap contributors. Tiles from Mapbox .';
57 | let mapbox = new L.TileLayer(mapboxUrl, {
58 | attribution: mapboxAttrib,
59 | tileSize: 512,
60 | zoomOffset: -1
61 | });
62 |
63 | let map = new L.Map("map", {
64 | layers: [getCurrentTheme() === "dark" ? osmDark : osm],
65 | center: [51.505, -0.09],
66 | zoom: 10,
67 | zoomControl: true
68 | });
69 |
70 | // add location control to global name space for testing only
71 | // on a production site, omit the "lc = "!
72 | lc = L.control
73 | .locate({
74 | strings: {
75 | title: "Show me where I am, yo!"
76 | }
77 | })
78 | .addTo(map);
79 |
--------------------------------------------------------------------------------
/dist/L.Control.Locate.min.css:
--------------------------------------------------------------------------------
1 | .leaflet-control-locate{--locate-control-icon-color:black;--locate-control-active-color:#2074b6;--locate-control-following-color:#fc8428}.leaflet-control-locate a .leaflet-control-locate-location-arrow{background-color:var(--locate-control-icon-color);width:16px;height:16px;margin:7px;display:inline-block;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.leaflet-control-locate a .leaflet-control-locate-spinner{background-color:var(--locate-control-icon-color);width:16px;height:16px;margin:7px;display:inline-block;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.leaflet-control-locate a .leaflet-control-locate-location-arrow{-webkit-mask-image:url("data:image/svg+xml;charset=UTF-8, ");mask-image:url("data:image/svg+xml;charset=UTF-8, ")}.leaflet-control-locate a .leaflet-control-locate-spinner{animation:2s linear infinite leaflet-control-locate-spin;-webkit-mask-image:url("data:image/svg+xml;charset=UTF-8, ");mask-image:url("data:image/svg+xml;charset=UTF-8, ")}.leaflet-control-locate.active a .leaflet-control-locate-location-arrow{background-color:var(--locate-control-active-color)}.leaflet-control-locate.following a .leaflet-control-locate-location-arrow{background-color:var(--locate-control-following-color)}.leaflet-touch .leaflet-bar .leaflet-locate-text-active{text-overflow:ellipsis;white-space:nowrap;width:100%;max-width:200px;padding:0 10px;overflow:hidden}.leaflet-touch .leaflet-bar .leaflet-locate-text-active .leaflet-locate-icon{padding:0 5px 0 0}.leaflet-control-locate-location circle{animation:4s infinite leaflet-control-locate-throb}@keyframes leaflet-control-locate-throb{0%{stroke-width:1px}50%{stroke-width:3px;transform:scale(.8)}to{stroke-width:1px}}@keyframes leaflet-control-locate-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}
2 | /*# sourceMappingURL=dist/L.Control.Locate.min.css.map */
3 |
--------------------------------------------------------------------------------
/dist/L.Control.Locate.mapbox.min.css:
--------------------------------------------------------------------------------
1 | .leaflet-control-locate{--locate-control-icon-color:black;--locate-control-active-color:#2074b6;--locate-control-following-color:#fc8428}.leaflet-control-locate a .leaflet-control-locate-location-arrow{background-color:var(--locate-control-icon-color);width:16px;height:16px;margin:7px;display:inline-block;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.leaflet-control-locate a .leaflet-control-locate-spinner{background-color:var(--locate-control-icon-color);width:16px;height:16px;margin:7px;display:inline-block;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.leaflet-control-locate a .leaflet-control-locate-location-arrow{-webkit-mask-image:url("data:image/svg+xml;charset=UTF-8, ");mask-image:url("data:image/svg+xml;charset=UTF-8, ")}.leaflet-control-locate a .leaflet-control-locate-spinner{animation:2s linear infinite leaflet-control-locate-spin;-webkit-mask-image:url("data:image/svg+xml;charset=UTF-8, ");mask-image:url("data:image/svg+xml;charset=UTF-8, ")}.leaflet-control-locate.active a .leaflet-control-locate-location-arrow{background-color:var(--locate-control-active-color)}.leaflet-control-locate.following a .leaflet-control-locate-location-arrow{background-color:var(--locate-control-following-color)}.leaflet-touch .leaflet-bar .leaflet-locate-text-active{text-overflow:ellipsis;white-space:nowrap;width:100%;max-width:200px;padding:0 10px;overflow:hidden}.leaflet-touch .leaflet-bar .leaflet-locate-text-active .leaflet-locate-icon{padding:0 5px 0 0}.leaflet-control-locate-location circle{animation:4s infinite leaflet-control-locate-throb}@keyframes leaflet-control-locate-throb{0%{stroke-width:1px}50%{stroke-width:3px;transform:scale(.8)}to{stroke-width:1px}}@keyframes leaflet-control-locate-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.leaflet-control-locate a .leaflet-control-locate-location-arrow,.leaflet-control-locate a .leaflet-control-locate-spinner{margin:5px}
2 | /*# sourceMappingURL=dist/L.Control.Locate.mapbox.min.css.map */
3 |
--------------------------------------------------------------------------------
/demo/style.css:
--------------------------------------------------------------------------------
1 | /* Dark mode - controlled via data-theme attribute set by JS */
2 | :root[data-theme="dark"] .leaflet-control-locate {
3 | --locate-control-icon-color: #fff;
4 | }
5 |
6 | :root[data-theme="dark"] #dark-mode-toggle,
7 | :root[data-theme="dark"] .leaflet-bar a,
8 | :root[data-theme="dark"] .leaflet-popup-content-wrapper,
9 | :root[data-theme="dark"] .leaflet-popup-tip,
10 | :root[data-theme="dark"] .leaflet-control-attribution {
11 | background-color: #3a3a3a;
12 | color: #fff;
13 | }
14 |
15 | :root[data-theme="dark"] .leaflet-control-attribution a {
16 | color: #6db3f2;
17 | }
18 |
19 | :root[data-theme="dark"] .leaflet-bar a {
20 | border-color: rgba(255, 255, 255, 0.2);
21 | }
22 |
23 | :root[data-theme="dark"] #dark-mode-toggle:hover,
24 | :root[data-theme="dark"] .leaflet-bar a:hover {
25 | background-color: #555;
26 | }
27 |
28 | html[data-theme="dark"],
29 | html[data-theme="dark"] body {
30 | background-color: #1a1a1a;
31 | color: #fff;
32 | }
33 |
34 | html,
35 | body {
36 | padding: 0;
37 | margin: 0;
38 | background-color: #fff;
39 | color: #000;
40 | transition: background-color 0.3s ease, color 0.3s ease;
41 | }
42 |
43 | #map {
44 | position: absolute;
45 | width: 100%;
46 | height: 100%;
47 | }
48 |
49 | #dark-mode-toggle {
50 | position: fixed;
51 | top: 130px;
52 | left: 10px;
53 | z-index: 2000;
54 | background: #fff;
55 | border-radius: 4px;
56 | width: 34px;
57 | height: 34px;
58 | padding: 0;
59 | cursor: pointer;
60 | font-size: 18px;
61 | line-height: 26px;
62 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
63 | transition: background-color 0.3s ease;
64 | display: flex;
65 | align-items: center;
66 | justify-content: center;
67 | }
68 |
69 | /*
70 | The part below is for a fancy "Fork me on GitHub" ribbon in the top right corner.
71 | It was created with the help of https://github.com/codepo8/css-fork-on-github-ribbon.
72 | */
73 |
74 | #forkmeongithub a {
75 | background: #bb1111cc;
76 | color: #fff;
77 | font-family: arial, sans-serif;
78 | font-size: 1rem;
79 | font-weight: bold;
80 | text-align: center;
81 | text-decoration: none;
82 | text-shadow: 2px 2px #00000055;
83 | line-height: 1.4rem;
84 | padding: 5px 40px;
85 | top: -150px;
86 | }
87 |
88 | #forkmeongithub a:hover {
89 | background: #333388bb;
90 | }
91 |
92 | #forkmeongithub a::before,
93 | #forkmeongithub a::after {
94 | content: "";
95 | width: 100%;
96 | display: block;
97 | position: absolute;
98 | top: 1px;
99 | left: 0;
100 | height: 1px;
101 | background: #fff;
102 | }
103 |
104 | #forkmeongithub a::after {
105 | bottom: 1px;
106 | top: auto;
107 | }
108 |
109 | @media screen and (min-width: 800px) {
110 | #forkmeongithub {
111 | position: fixed;
112 | display: block;
113 | top: -10px;
114 | right: -10px;
115 | width: 200px;
116 | overflow: hidden;
117 | height: 200px;
118 | z-index: 9999;
119 | }
120 | #forkmeongithub a {
121 | width: 200px;
122 | position: absolute;
123 | top: 60px;
124 | right: -60px;
125 | transform: rotate(45deg);
126 | box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.8);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/demo_esm/style.css:
--------------------------------------------------------------------------------
1 | /* Dark mode - controlled via data-theme attribute set by JS */
2 | :root[data-theme="dark"] .leaflet-control-locate {
3 | --locate-control-icon-color: #fff;
4 | }
5 |
6 | :root[data-theme="dark"] #dark-mode-toggle,
7 | :root[data-theme="dark"] .leaflet-bar a,
8 | :root[data-theme="dark"] .leaflet-popup-content-wrapper,
9 | :root[data-theme="dark"] .leaflet-popup-tip,
10 | :root[data-theme="dark"] .leaflet-control-attribution {
11 | background-color: #3a3a3a;
12 | color: #fff;
13 | }
14 |
15 | :root[data-theme="dark"] .leaflet-control-attribution a {
16 | color: #6db3f2;
17 | }
18 |
19 | :root[data-theme="dark"] .leaflet-bar a {
20 | border-color: rgba(255, 255, 255, 0.2);
21 | }
22 |
23 | :root[data-theme="dark"] #dark-mode-toggle:hover,
24 | :root[data-theme="dark"] .leaflet-bar a:hover {
25 | background-color: #555;
26 | }
27 |
28 | html[data-theme="dark"],
29 | html[data-theme="dark"] body {
30 | background-color: #181818;
31 | color: #fff;
32 | }
33 |
34 | html,
35 | body {
36 | padding: 0;
37 | margin: 0;
38 | background-color: #fff;
39 | color: #000;
40 | transition: background-color 0.3s ease, color 0.3s ease;
41 | }
42 |
43 | #map {
44 | position: absolute;
45 | width: 100%;
46 | height: 100%;
47 | }
48 |
49 | #dark-mode-toggle {
50 | position: fixed;
51 | top: 130px;
52 | left: 10px;
53 | z-index: 2000;
54 | background: #fff;
55 | border-radius: 4px;
56 | width: 34px;
57 | height: 34px;
58 | padding: 0;
59 | cursor: pointer;
60 | font-size: 18px;
61 | line-height: 26px;
62 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
63 | transition: background-color 0.3s ease;
64 | display: flex;
65 | align-items: center;
66 | justify-content: center;
67 | }
68 |
69 | /*
70 | The part below is for a fancy "Fork me on GitHub" ribbon in the top right corner.
71 | It was created with the help of https://github.com/codepo8/css-fork-on-github-ribbon.
72 | */
73 |
74 | #forkmeongithub a {
75 | background: #bb1111cc;
76 | color: #fff;
77 | font-family: arial, sans-serif;
78 | font-size: 1rem;
79 | font-weight: bold;
80 | text-align: center;
81 | text-decoration: none;
82 | text-shadow: 2px 2px #00000055;
83 | line-height: 1.4rem;
84 | padding: 5px 40px;
85 | top: -150px;
86 | }
87 |
88 | #forkmeongithub a:hover {
89 | background: #333388bb;
90 | }
91 |
92 | #forkmeongithub a::before,
93 | #forkmeongithub a::after {
94 | content: "";
95 | width: 100%;
96 | display: block;
97 | position: absolute;
98 | top: 1px;
99 | left: 0;
100 | height: 1px;
101 | background: #fff;
102 | }
103 |
104 | #forkmeongithub a::after {
105 | bottom: 1px;
106 | top: auto;
107 | }
108 |
109 | @media screen and (min-width: 800px) {
110 | #forkmeongithub {
111 | position: fixed;
112 | display: block;
113 | top: -10px;
114 | right: -10px;
115 | width: 200px;
116 | overflow: hidden;
117 | height: 200px;
118 | z-index: 9999;
119 | }
120 | #forkmeongithub a {
121 | width: 200px;
122 | position: absolute;
123 | top: 60px;
124 | right: -60px;
125 | transform: rotate(45deg);
126 | box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.8);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/dist/L.Control.Locate.css:
--------------------------------------------------------------------------------
1 | .leaflet-control-locate {
2 | --locate-control-icon-color: black;
3 | --locate-control-active-color: #2074b6;
4 | --locate-control-following-color: #fc8428;
5 | }
6 |
7 | .leaflet-control-locate a .leaflet-control-locate-location-arrow {
8 | background-color: var(--locate-control-icon-color);
9 | width: 16px;
10 | height: 16px;
11 | margin: 7px;
12 | display: inline-block;
13 | -webkit-mask-position: center;
14 | mask-position: center;
15 | -webkit-mask-repeat: no-repeat;
16 | mask-repeat: no-repeat;
17 | }
18 |
19 | .leaflet-control-locate a .leaflet-control-locate-spinner {
20 | background-color: var(--locate-control-icon-color);
21 | width: 16px;
22 | height: 16px;
23 | margin: 7px;
24 | display: inline-block;
25 | -webkit-mask-position: center;
26 | mask-position: center;
27 | -webkit-mask-repeat: no-repeat;
28 | mask-repeat: no-repeat;
29 | }
30 |
31 | .leaflet-control-locate a .leaflet-control-locate-location-arrow {
32 | -webkit-mask-image: url("data:image/svg+xml;charset=UTF-8, ");
33 | mask-image: url("data:image/svg+xml;charset=UTF-8, ");
34 | }
35 |
36 | .leaflet-control-locate a .leaflet-control-locate-spinner {
37 | animation: 2s linear infinite leaflet-control-locate-spin;
38 | -webkit-mask-image: url("data:image/svg+xml;charset=UTF-8, ");
39 | mask-image: url("data:image/svg+xml;charset=UTF-8, ");
40 | }
41 |
42 | .leaflet-control-locate.active a .leaflet-control-locate-location-arrow {
43 | background-color: var(--locate-control-active-color);
44 | }
45 |
46 | .leaflet-control-locate.following a .leaflet-control-locate-location-arrow {
47 | background-color: var(--locate-control-following-color);
48 | }
49 |
50 | .leaflet-touch .leaflet-bar .leaflet-locate-text-active {
51 | text-overflow: ellipsis;
52 | white-space: nowrap;
53 | width: 100%;
54 | max-width: 200px;
55 | padding: 0 10px;
56 | overflow: hidden;
57 | }
58 |
59 | .leaflet-touch .leaflet-bar .leaflet-locate-text-active .leaflet-locate-icon {
60 | padding: 0 5px 0 0;
61 | }
62 |
63 | .leaflet-control-locate-location circle {
64 | animation: 4s infinite leaflet-control-locate-throb;
65 | }
66 |
67 | @keyframes leaflet-control-locate-throb {
68 | 0% {
69 | stroke-width: 1px;
70 | }
71 |
72 | 50% {
73 | stroke-width: 3px;
74 | transform: scale(.8);
75 | }
76 |
77 | 100% {
78 | stroke-width: 1px;
79 | }
80 | }
81 |
82 | @keyframes leaflet-control-locate-spin {
83 | 0% {
84 | transform: rotate(0);
85 | }
86 |
87 | 100% {
88 | transform: rotate(360deg);
89 | }
90 | }
91 |
92 | /*# sourceMappingURL=dist/L.Control.Locate.css.map */
93 |
--------------------------------------------------------------------------------
/dist/L.Control.Locate.mapbox.css:
--------------------------------------------------------------------------------
1 | .leaflet-control-locate {
2 | --locate-control-icon-color: black;
3 | --locate-control-active-color: #2074b6;
4 | --locate-control-following-color: #fc8428;
5 | }
6 |
7 | .leaflet-control-locate a .leaflet-control-locate-location-arrow {
8 | background-color: var(--locate-control-icon-color);
9 | width: 16px;
10 | height: 16px;
11 | margin: 7px;
12 | display: inline-block;
13 | -webkit-mask-position: center;
14 | mask-position: center;
15 | -webkit-mask-repeat: no-repeat;
16 | mask-repeat: no-repeat;
17 | }
18 |
19 | .leaflet-control-locate a .leaflet-control-locate-spinner {
20 | background-color: var(--locate-control-icon-color);
21 | width: 16px;
22 | height: 16px;
23 | margin: 7px;
24 | display: inline-block;
25 | -webkit-mask-position: center;
26 | mask-position: center;
27 | -webkit-mask-repeat: no-repeat;
28 | mask-repeat: no-repeat;
29 | }
30 |
31 | .leaflet-control-locate a .leaflet-control-locate-location-arrow {
32 | -webkit-mask-image: url("data:image/svg+xml;charset=UTF-8, ");
33 | mask-image: url("data:image/svg+xml;charset=UTF-8, ");
34 | }
35 |
36 | .leaflet-control-locate a .leaflet-control-locate-spinner {
37 | animation: 2s linear infinite leaflet-control-locate-spin;
38 | -webkit-mask-image: url("data:image/svg+xml;charset=UTF-8, ");
39 | mask-image: url("data:image/svg+xml;charset=UTF-8, ");
40 | }
41 |
42 | .leaflet-control-locate.active a .leaflet-control-locate-location-arrow {
43 | background-color: var(--locate-control-active-color);
44 | }
45 |
46 | .leaflet-control-locate.following a .leaflet-control-locate-location-arrow {
47 | background-color: var(--locate-control-following-color);
48 | }
49 |
50 | .leaflet-touch .leaflet-bar .leaflet-locate-text-active {
51 | text-overflow: ellipsis;
52 | white-space: nowrap;
53 | width: 100%;
54 | max-width: 200px;
55 | padding: 0 10px;
56 | overflow: hidden;
57 | }
58 |
59 | .leaflet-touch .leaflet-bar .leaflet-locate-text-active .leaflet-locate-icon {
60 | padding: 0 5px 0 0;
61 | }
62 |
63 | .leaflet-control-locate-location circle {
64 | animation: 4s infinite leaflet-control-locate-throb;
65 | }
66 |
67 | @keyframes leaflet-control-locate-throb {
68 | 0% {
69 | stroke-width: 1px;
70 | }
71 |
72 | 50% {
73 | stroke-width: 3px;
74 | transform: scale(.8);
75 | }
76 |
77 | 100% {
78 | stroke-width: 1px;
79 | }
80 | }
81 |
82 | @keyframes leaflet-control-locate-spin {
83 | 0% {
84 | transform: rotate(0);
85 | }
86 |
87 | 100% {
88 | transform: rotate(360deg);
89 | }
90 | }
91 |
92 | .leaflet-control-locate a .leaflet-control-locate-location-arrow, .leaflet-control-locate a .leaflet-control-locate-spinner {
93 | margin: 5px;
94 | }
95 |
96 | /*# sourceMappingURL=dist/L.Control.Locate.mapbox.css.map */
97 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | leaflet-locatecontrol
7 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
221 |
222 |
223 | Demos
224 |
235 |
236 |
237 |
238 |
--------------------------------------------------------------------------------
/dist/L.Control.Locate.min.js:
--------------------------------------------------------------------------------
1 | /*! Version: 0.85.1
2 | Copyright (c) 2016 Dominik Moritz */
3 |
4 | !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,i){"use strict";const s=(t,i)=>{i.split(" ").forEach(i=>{t.classList.add(i)})},o=(t,i)=>{i.split(" ").forEach(i=>{t.classList.remove(i)})},e=i.Marker.extend({initialize(t,s){i.setOptions(this,s),this._latlng=t,this.createIcon()},createIcon(){const t=this.options,s=[["stroke",t.color],["stroke-width",t.weight],["fill",t.fillColor],["fill-opacity",t.fillOpacity],["opacity",t.opacity]].filter(([t,i])=>void 0!==i).map(([t,i])=>`${t}="${i}"`).join(" "),o=this._getIconSVG(t,s);this._locationIcon=i.divIcon({className:o.className,html:o.svg,iconSize:[o.w,o.h]}),this.setIcon(this._locationIcon)},_getIconSVG(t,i){const s=t.radius,o=s+t.weight,e=2*o;return{className:"leaflet-control-locate-location",svg:` `,w:e,h:e}},setStyle(t){i.setOptions(this,t),this.createIcon()}}),n=e.extend({initialize(t,s,o){i.setOptions(this,o),this._latlng=t,this._heading=s,this.createIcon()},setHeading(t){this._heading=t},_getIconSVG(t,i){const s=t.radius,o=s+t.weight+t.depth,e=2*o;return{className:"leaflet-control-locate-heading",svg:` `,w:e,h:e}},_arrowPoints(t,i,s,o){const e=(o-90)*Math.PI/180,n=Math.cos(e),a=Math.sin(e),l=-Math.sin(e),h=Math.cos(e),r=i/2,c=t*n,p=t*a;return`M ${c+r*l},${p+r*h} L ${c-r*l},${p-r*h} L ${c+s*n},${p+s*a} Z`}}),a=i.Control.extend({options:{position:"topleft",layer:void 0,setView:"untilPanOrZoom",keepCurrentZoomLevel:!1,initialZoomLevel:!1,getLocationBounds:t=>t.bounds,flyTo:!1,clickBehavior:{inView:"stop",outOfView:"setView",inViewNotFollowing:"inView"},returnToPrevBounds:!1,cacheLocation:!0,drawCircle:!0,drawMarker:!0,showCompass:!0,markerClass:e,compassClass:n,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,s){const o=i.DomUtil.create("a","leaflet-bar-part leaflet-bar-part-single",t);o.title=s.strings.title,o.href="#",o.setAttribute("role","button");const e=i.DomUtil.create(s.iconElementTag,s.icon,o);if(void 0!==s.strings.text){i.DomUtil.create(s.textElementTag,"leaflet-locate-text",o).textContent=s.strings.text,o.classList.add("leaflet-locate-text-active"),o.parentNode.style.display="flex",s.icon.length>0&&e.classList.add("leaflet-locate-icon")}return{link:o,icon:e}},onLocationError(t,i){alert(t.message)},onLocationOutsideMapBounds(t){t.stop(),alert(t.options.strings.outsideMapBoundsMsg)},showPopup:!0,strings:{title:"Show me where I am",metersUnit:"meters",feetUnit:"feet",popup:"You are within {distance} {unit} from this point",outsideMapBoundsMsg:"You seem located outside the boundaries of the map"},locateOptions:{maxZoom:1/0,watch:!0,setView:!1}},initialize(t){for(const s in t)"object"==typeof this.options[s]?i.extend(this.options[s],t[s]):this.options[s]=t[s];this.options.followMarkerStyle=i.extend({},this.options.markerStyle,this.options.followMarkerStyle),this.options.followCircleStyle=i.extend({},this.options.circleStyle,this.options.followCircleStyle),this.options.followCompassStyle=i.extend({},this.options.compassStyle,this.options.followCompassStyle)},onAdd(t){const s=i.DomUtil.create("div","leaflet-control-locate leaflet-bar leaflet-control");this._container=s,this._map=t,this._layer=this.options.layer||new i.LayerGroup,this._layer.addTo(t),this._event=void 0,this._compassHeading=null,this._prevBounds=null;const o=this.options.createButtonCallback(s,this.options);return this._link=o.link,this._icon=o.icon,i.DomEvent.on(this._link,"click",function(t){i.DomEvent.stopPropagation(t),i.DomEvent.preventDefault(t),this._onClick()},this).on(this._link,"dblclick",i.DomEvent.stopPropagation),this._resetVariables(),this._map.on("unload",this._unload,this),s},_onClick(){this._justClicked=!0;const t=this._isFollowing();if(this._userPanned=!1,this._userZoomed=!1,this._active&&!this._event)this.stop();else if(this._active){const i=this.options.clickBehavior;let s=i.outOfView;switch(this._map.getBounds().contains(this._event.latlng)&&(s=t?i.inView:i.inViewNotFollowing),i[s]&&(s=i[s]),s){case"setView":this.setView();break;case"stop":if(this.stop(),this.options.returnToPrevBounds){(this.options.flyTo?this._map.flyToBounds:this._map.fitBounds).bind(this._map)(this._prevBounds)}}}else this.options.returnToPrevBounds&&(this._prevBounds=this._map.getBounds()),this.start();this._updateContainerStyle()},start(){this._activate(),this._event&&(this._drawMarker(this._map),this.options.setView&&this.setView()),this._updateContainerStyle()},stop(){this._deactivate(),this._cleanClasses(),this._resetVariables(),this._removeMarker()},stopFollowing(){this._userPanned=!0,this._updateContainerStyle(),this._drawMarker()},_activate(){if(!this._active&&this._map&&(this._map.locate(this.options.locateOptions),this._map.fire("locateactivate",this),this._active=!0,this._map.on("locationfound",this._onLocationFound,this),this._map.on("locationerror",this._onLocationError,this),this._map.on("dragstart",this._onDrag,this),this._map.on("zoomstart",this._onZoom,this),this._map.on("zoomend",this._onZoomEnd,this),this.options.showCompass)){const t="ondeviceorientationabsolute"in window;if(t||"ondeviceorientation"in window){const s=this,o=function(){i.DomEvent.on(window,t?"deviceorientationabsolute":"deviceorientation",s._onDeviceOrientation,s)};DeviceOrientationEvent&&"function"==typeof DeviceOrientationEvent.requestPermission?DeviceOrientationEvent.requestPermission().then(function(t){"granted"===t&&o()}):o()}}},_deactivate(){this._active&&this._map&&(this._map.stopLocate(),this._map.fire("locatedeactivate",this),this._active=!1,this.options.cacheLocation||(this._event=void 0),this._map.off("locationfound",this._onLocationFound,this),this._map.off("locationerror",this._onLocationError,this),this._map.off("dragstart",this._onDrag,this),this._map.off("zoomstart",this._onZoom,this),this._map.off("zoomend",this._onZoomEnd,this),this.options.showCompass&&(this._compassHeading=null,"ondeviceorientationabsolute"in window?i.DomEvent.off(window,"deviceorientationabsolute",this._onDeviceOrientation,this):"ondeviceorientation"in window&&i.DomEvent.off(window,"deviceorientation",this._onDeviceOrientation,this)))},setView(){if(this._drawMarker(),this._isOutsideMapBounds())this._event=void 0,this.options.onLocationOutsideMapBounds(this);else if(this._justClicked&&!1!==this.options.initialZoomLevel){(this.options.flyTo?this._map.flyTo:this._map.setView).bind(this._map)([this._event.latitude,this._event.longitude],this.options.initialZoomLevel)}else if(this.options.keepCurrentZoomLevel){(this.options.flyTo?this._map.flyTo:this._map.panTo).bind(this._map)([this._event.latitude,this._event.longitude])}else{let t=this.options.flyTo?this._map.flyToBounds:this._map.fitBounds;this._ignoreEvent=!0,t.bind(this._map)(this.options.getLocationBounds(this._event),{padding:this.options.circlePadding,maxZoom:this.options.initialZoomLevel||this.options.locateOptions.maxZoom}),i.Util.requestAnimFrame(function(){this._ignoreEvent=!1},this)}},_drawCompass(){if(!this._event)return;const t=this._event.latlng;if(this.options.showCompass&&t&&null!==this._compassHeading){const i=this._isFollowing()?this.options.followCompassStyle:this.options.compassStyle;this._compass?(this._compass.setLatLng(t),this._compass.setHeading(this._compassHeading),this._compass.setStyle&&this._compass.setStyle(i)):this._compass=new this.options.compassClass(t,this._compassHeading,i).addTo(this._layer)}!this._compass||this.options.showCompass&&null!==this._compassHeading||(this._compass.removeFrom(this._layer),this._compass=null)},_drawMarker(){void 0===this._event.accuracy&&(this._event.accuracy=0);const t=this._event.accuracy,s=this._event.latlng;if(this.options.drawCircle){const o=this._isFollowing()?this.options.followCircleStyle:this.options.circleStyle;this._circle?this._circle.setLatLng(s).setRadius(t).setStyle(o):this._circle=i.circle(s,t,o).addTo(this._layer)}let o,e;if(this.options.metric?(o=t.toFixed(0),e=this.options.strings.metersUnit):(o=(3.2808399*t).toFixed(0),e=this.options.strings.feetUnit),this.options.drawMarker){const t=this._isFollowing()?this.options.followMarkerStyle:this.options.markerStyle;this._marker?(this._marker.setLatLng(s),this._marker.setStyle&&this._marker.setStyle(t)):this._marker=new this.options.markerClass(s,t).addTo(this._layer)}this._drawCompass();const n=this.options.strings.popup;function a(){return"string"==typeof n?i.Util.template(n,{distance:o,unit:e}):"function"==typeof n?n({distance:o,unit:e}):n}this.options.showPopup&&n&&this._marker&&this._marker.bindPopup(a())._popup.setLatLng(s),this.options.showPopup&&n&&this._compass&&this._compass.bindPopup(a())._popup.setLatLng(s)},_removeMarker(){this._layer.clearLayers(),this._marker=void 0,this._circle=void 0},_unload(){this.stop(),this._map&&this._map.off("unload",this._unload,this)},_setCompassHeading(t){!isNaN(parseFloat(t))&&isFinite(t)?(t=Math.round(t),this._compassHeading=t,i.Util.requestAnimFrame(this._drawCompass,this)):this._compassHeading=null},_onCompassNeedsCalibration(){this._setCompassHeading()},_onDeviceOrientation(t){this._active&&(t.webkitCompassHeading?this._setCompassHeading(t.webkitCompassHeading):t.absolute&&t.alpha&&this._setCompassHeading(360-t.alpha))},_onLocationError(t){3==t.code&&this.options.locateOptions.watch||(this.stop(),this.options.onLocationError(t,this))},_onLocationFound(t){if((!this._event||this._event.latlng.lat!==t.latlng.lat||this._event.latlng.lng!==t.latlng.lng||this._event.accuracy!==t.accuracy)&&this._active){switch(this._event=t,this._drawMarker(),this._updateContainerStyle(),this.options.setView){case"once":this._justClicked&&this.setView();break;case"untilPan":this._userPanned||this.setView();break;case"untilPanOrZoom":this._userPanned||this._userZoomed||this.setView();break;case"always":this.setView()}this._justClicked=!1}},_onDrag(){this._event&&!this._ignoreEvent&&(this._userPanned=!0,this._updateContainerStyle(),this._drawMarker())},_onZoom(){this._event&&!this._ignoreEvent&&(this._userZoomed=!0,this._updateContainerStyle(),this._drawMarker())},_onZoomEnd(){this._event&&this._drawCompass(),this._event&&!this._ignoreEvent&&this._marker&&!this._map.getBounds().pad(-.3).contains(this._marker.getLatLng())&&(this._userPanned=!0,this._updateContainerStyle(),this._drawMarker())},_isFollowing(){return!!this._active&&("always"===this.options.setView||("untilPan"===this.options.setView?!this._userPanned:"untilPanOrZoom"===this.options.setView?!this._userPanned&&!this._userZoomed:void 0))},_isOutsideMapBounds(){return void 0!==this._event&&(this._map.options.maxBounds&&!this._map.options.maxBounds.contains(this._event.latlng))},_updateContainerStyle(){this._container&&(this._active&&!this._event?this._setClasses("requesting"):this._isFollowing()?this._setClasses("following"):this._active?this._setClasses("active"):this._cleanClasses())},_setClasses(t){"requesting"==t?(o(this._container,"active following"),s(this._container,"requesting"),o(this._icon,this.options.icon),s(this._icon,this.options.iconLoading)):"active"==t?(o(this._container,"requesting following"),s(this._container,"active"),o(this._icon,this.options.iconLoading),s(this._icon,this.options.icon)):"following"==t&&(o(this._container,"requesting"),s(this._container,"active following"),o(this._icon,this.options.iconLoading),s(this._icon,this.options.icon))},_cleanClasses(){i.DomUtil.removeClass(this._container,"requesting"),i.DomUtil.removeClass(this._container,"active"),i.DomUtil.removeClass(this._container,"following"),o(this._icon,this.options.iconLoading),s(this._icon,this.options.icon)},_resetVariables(){this._active=!1,this._justClicked=!1,this._userPanned=!1,this._userZoomed=!1}});t.CompassMarker=n,t.LocateControl=a,t.LocationMarker=e,t.locate=function(t){return new a(t)},Object.defineProperty(t,"__esModule",{value:!0})}),"undefined"!=typeof window&&window.L&&(window.L.control=window.L.control||{},window.L.control.locate=window.L.Control.Locate.locate);
5 | //# sourceMappingURL=L.Control.Locate.min.js.map
6 |
--------------------------------------------------------------------------------
/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 | Check out the [demo page](https://domoritz.github.io/leaflet-locatecontrol/) with three different examples (Leaflet UMD, Leaflet ESM, Mapbox UMD).
15 |
16 | ## Basic Usage
17 |
18 | ### Set up
19 |
20 | 1. Get the JavaScript and CSS files
21 | 2. Include the files in your project
22 | 3. Initialize the plugin
23 |
24 | #### Get the JavaScript and CSS files
25 |
26 | **For production:**
27 |
28 | The best way to get the plugin is via [npm](https://www.npmjs.org/):
29 |
30 | ```bash
31 | npm install leaflet.locatecontrol
32 | ```
33 |
34 | Alternatively, you can use the [JsDelivr CDN](https://www.jsdelivr.com/projects/leaflet.locatecontrol) (see instructions below) or [download the files from this repository](https://github.com/domoritz/leaflet-locatecontrol/archive/gh-pages.zip).
35 |
36 | **For development:**
37 |
38 | Clone the repository to work with the source code:
39 |
40 | ```bash
41 | git clone https://github.com/domoritz/leaflet-locatecontrol
42 | ```
43 |
44 | The source files are in `src/` and the built distribution files are in `dist/`.
45 |
46 | #### Include the JavaScript and CSS files in your project
47 |
48 | ##### With CDN
49 |
50 | 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.
51 |
52 | ```html
53 |
54 |
55 | ```
56 |
57 | ##### With `npm`
58 |
59 | ```ts
60 | import "leaflet.locatecontrol"; // Import plugin
61 | import "leaflet.locatecontrol/dist/L.Control.Locate.min.css"; // Import styles
62 | import L from "leaflet"; // Import L from leaflet to start using the plugin
63 | ```
64 |
65 | If you are using a bundler or esm, use
66 |
67 | ```ts
68 | import { LocateControl } from "leaflet.locatecontrol";
69 | import "leaflet.locatecontrol/dist/L.Control.Locate.min.css";
70 | ```
71 |
72 | Then use `new LocateControl()` instead of `L.control.locate()`.
73 |
74 | #### Add the following snippet to your map initialization
75 |
76 | This snippet adds the control to the map. You can pass also pass a configuration.
77 |
78 | ```js
79 | L.control.locate().addTo(map);
80 | ```
81 |
82 | ### Possible options
83 |
84 | The locate controls inherits options from [Leaflet Controls](http://leafletjs.com/reference.html#control-options).
85 |
86 | To customize the control, pass an object with your custom options to the locate control.
87 |
88 | ```js
89 | L.control.locate(OPTIONS).addTo(map);
90 | ```
91 |
92 | 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).
93 |
94 |
95 | | Option | Type | Description | Default |
96 | |------------|-----------|-------------------|----------|
97 | | `position` | `string` | Position of the control | `topleft` |
98 | | `layer` | [`ILayer`](http://leafletjs.com/reference.html#ilayer) | The layer that the user's location should be drawn on. | a new layer |
99 | | `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'` |
100 | | `flyTo` | `boolean` | Smooth pan and zoom to the location of the marker. Only works in Leaflet 1.0+. | `false` |
101 | | `keepCurrentZoomLevel` | `boolean` | Only pan when setting the view. | `false` |
102 | | `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` |
103 | | `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'}` |
104 | | `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` |
105 | | `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` |
106 | | `showCompass` | `boolean` | Show the compass bearing on top of the location marker | `true` |
107 | | `drawCircle` | `boolean` | If set, a circle that shows the location accuracy is drawn. | `true` |
108 | | `drawMarker` | `boolean` | If set, the marker at the users' location is drawn. | `true` |
109 | | `markerClass` | `class` | The class to be used to create the marker. | `LocationMarker` |
110 | | `compassClass` | `class` | The class to be used to create the marker. | `CompassMarker` |
111 | | `circleStyle` | [`Path options`](http://leafletjs.com/reference.html#path-options) | Accuracy circle style properties. | see code |
112 | | `markerStyle` | [`Path options`](http://leafletjs.com/reference.html#path-options) | Inner marker style properties. Only works if your marker class supports `setStyle`. | see code |
113 | | `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 |
114 | | `followCircleStyle` | [`Path options`](http://leafletjs.com/reference.html#path-options) | Changes to the accuracy circle while following. Only need to provide changes. | `{}` |
115 | | `followMarkerStyle` | [`Path options`](http://leafletjs.com/reference.html#path-options) | Changes to the inner marker while following. Only need to provide changes. | `{}` |
116 | | `followCompassStyle` | [`Path options`](http://leafletjs.com/reference.html#path-options) | Changes to the compass marker while following. Only need to provide changes. | `{}` |
117 | | `icon` | `string` | The CSS class for the icon. | `leaflet-control-locate-location-arrow` |
118 | | `iconLoading` | `string` | The CSS class for the icon while loading. | `leaflet-control-locate-spinner` |
119 | | `iconElementTag` | `string` | The element to be created for icons. | `span` |
120 | | `circlePadding` | `array` | Padding around the accuracy circle. | `[0, 0]` |
121 | | `createButtonCallback` | `function` | This callback can be used in case you would like to override button creation behavior. | see code |
122 | | `getLocationBounds` | `function` | This callback can be used to override the viewport tracking behavior. | see code |
123 | | `onLocationError` | `function` | This even is called when the user's location is outside the bounds set on the map. | see code |
124 | | `metric` | `boolean` | Use metric units. | `true` |
125 | | `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 |
126 | | `showPopup` | `boolean` | Display a pop-up when the user click on the inner marker. | `true` |
127 | | `strings` | `object` | Strings used in the control. Options are `title`, `text`, `metersUnit`, `feetUnit`, `popup` and `outsideMapBoundsMsg` | see code |
128 | | `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 |
129 | | `locateOptions` | [`Locate options`](http://leafletjs.com/reference.html#map-locate-options) | The default options passed to leaflets locate method. | see code |
130 |
131 |
132 | For example, to customize the position and the title, you could write
133 |
134 | ```js
135 | var lc = L.control
136 | .locate({
137 | position: "topright",
138 | strings: {
139 | title: "Show me where I am, yo!"
140 | }
141 | })
142 | .addTo(map);
143 | ```
144 |
145 | ## Screenshot
146 |
147 | 
148 |
149 | ## Users
150 |
151 | Sites that use this locate control:
152 |
153 | - [OpenStreetMap](http://www.openstreetmap.org/) on the start page
154 | - [MapBox](https://www.mapbox.com/mapbox.js/example/v1.0.0/leaflet-locatecontrol/)
155 | - [wheelmap.org](http://wheelmap.org/map)
156 | - [OpenMensa](http://openmensa.org/)
157 | - [Maps Marker Pro](https://www.mapsmarker.com) (WordPress plugin)
158 | - [Bikemap](https://jackdougherty.github.io/bikemapcode/)
159 | - [MyRoutes](https://myroutes.io/)
160 | - [NearbyWiki](https://en.nearbywiki.org/)
161 | - ...
162 |
163 | ## Advanced Usage
164 |
165 | ### Methods
166 |
167 | You can call `start()` or `stop()` on the locate control object to set the location on page load for example.
168 |
169 | ```js
170 | // create control and add to map
171 | var lc = L.control.locate().addTo(map);
172 |
173 | // request location update and set location
174 | lc.start();
175 | ```
176 |
177 | You can keep the plugin active but stop following using `lc.stopFollowing()`.
178 |
179 | ### Events
180 |
181 | 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).
182 |
183 | Additionally, the locate control fires `locateactivate` and `locatedeactivate` events on the map object when it is activated and deactivated, respectively.
184 |
185 | ### Extending
186 |
187 | 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.
188 |
189 | ```js
190 | L.Control.MyLocate = L.Control.Locate.extend({
191 | _drawMarker: function () {
192 | // override to customize the marker
193 | }
194 | });
195 |
196 | var lc = new L.Control.MyLocate();
197 | ```
198 |
199 | ### FAQ
200 |
201 | #### How do I set the maximum zoom level?
202 |
203 | Set the `maxZoom` in `locateOptions` (`keepCurrentZoomLevel` must not be set to true).
204 |
205 | ```js
206 | map.addControl(
207 | L.control.locate({
208 | locateOptions: {
209 | maxZoom: 10
210 | }
211 | })
212 | );
213 | ```
214 |
215 | #### How do I enable high accuracy?
216 |
217 | To enable [high accuracy (GPS) mode](http://leafletjs.com/reference.html#map-enablehighaccuracy), set the `enableHighAccuracy` in `locateOptions`.
218 |
219 | ```js
220 | map.addControl(
221 | L.control.locate({
222 | locateOptions: {
223 | enableHighAccuracy: true
224 | }
225 | })
226 | );
227 | ```
228 |
229 | #### Safari does not work with Leaflet 1.7.1
230 |
231 | 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.
232 |
233 | ```js
234 | let map = new L.Map('map', {
235 | tap: false,
236 | ...
237 | });
238 | ```
239 |
240 | ## Developers
241 |
242 | Run the demo locally with `npm start` and then open [http://localhost:9000](http://localhost:9000).
243 |
244 | The development server is a native Node.js script (`scripts/server.js`) that serves the project on port 9000. It does not require any external dependencies. Note that modern browsers treat `localhost` as a secure context, so HTTPS is not required for the Geolocation API to work.
245 |
246 | To generate the minified JS and CSS files, run `npm run build`.
247 |
248 | ## Prettify and linting
249 |
250 | Before a Pull Request please check the code style.
251 |
252 | Run `npm run lint` to check if there are code style or linting issues.
253 |
254 | Run `npm run lint:fix` to automatically fix style and linting issues.
255 |
256 | ## Making a release (only core developer)
257 |
258 | A new version is released with `npm run bump:minor` or `npm run bump:patch`. Then push the changes with `git push && git push --tags` and publish to npm with `npm publish`.
259 |
260 | ### Terms
261 |
262 | - **active**: After we called `map.locate()` and before `map.stopLocate()`. Any time, the map can fire the `locationfound` or `locationerror` events.
263 | - **following**: Following refers to whether the map zooms and pans automatically when a new location is found.
264 |
265 | ## Thanks
266 |
267 | To all [contributors](https://github.com/domoritz/leaflet-locatecontrol/contributors) and issue reporters.
268 |
269 | ## License
270 |
271 | MIT
272 |
273 | 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)
274 |
--------------------------------------------------------------------------------
/dist/L.Control.Locate.esm.js:
--------------------------------------------------------------------------------
1 | import { Marker, setOptions, divIcon, Control, DomUtil, Util, circle, DomEvent, LayerGroup, extend } 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 |
10 | const addClasses = (el, names) => {
11 | names.split(" ").forEach((className) => {
12 | el.classList.add(className);
13 | });
14 | };
15 |
16 | const removeClasses = (el, names) => {
17 | names.split(" ").forEach((className) => {
18 | el.classList.remove(className);
19 | });
20 | };
21 |
22 | /**
23 | * Compatible with Circle but a true marker instead of a path
24 | */
25 | const LocationMarker = Marker.extend({
26 | initialize(latlng, options) {
27 | setOptions(this, options);
28 | this._latlng = latlng;
29 | this.createIcon();
30 | },
31 |
32 | /**
33 | * Create a styled circle location marker
34 | */
35 | createIcon() {
36 | const opt = this.options;
37 |
38 | const style = [
39 | ["stroke", opt.color],
40 | ["stroke-width", opt.weight],
41 | ["fill", opt.fillColor],
42 | ["fill-opacity", opt.fillOpacity],
43 | ["opacity", opt.opacity]
44 | ]
45 | .filter(([k,v]) => v !== undefined)
46 | .map(([k,v]) => `${k}="${v}"`)
47 | .join(" ");
48 |
49 | const icon = this._getIconSVG(opt, style);
50 |
51 | this._locationIcon = divIcon({
52 | className: icon.className,
53 | html: icon.svg,
54 | iconSize: [icon.w, icon.h]
55 | });
56 |
57 | this.setIcon(this._locationIcon);
58 | },
59 |
60 | /**
61 | * Return the raw svg for the shape
62 | *
63 | * Split so can be easily overridden
64 | */
65 | _getIconSVG(options, style) {
66 | const r = options.radius;
67 | const w = options.weight;
68 | const s = r + w;
69 | const s2 = s * 2;
70 | const svg =
71 | `` +
72 | ` `;
73 | return {
74 | className: "leaflet-control-locate-location",
75 | svg,
76 | w: s2,
77 | h: s2
78 | };
79 | },
80 |
81 | setStyle(style) {
82 | setOptions(this, style);
83 | this.createIcon();
84 | }
85 | });
86 |
87 | const CompassMarker = LocationMarker.extend({
88 | initialize(latlng, heading, options) {
89 | setOptions(this, options);
90 | this._latlng = latlng;
91 | this._heading = heading;
92 | this.createIcon();
93 | },
94 |
95 | setHeading(heading) {
96 | this._heading = heading;
97 | },
98 |
99 | /**
100 | * Create a styled arrow compass marker
101 | */
102 | _getIconSVG(options, style) {
103 | const r = options.radius;
104 | const s = r + options.weight + options.depth;
105 | const s2 = s * 2;
106 |
107 | const path = this._arrowPoints(r, options.width, options.depth, this._heading);
108 |
109 | const svg =
110 | `` +
111 | ` `;
112 | return {
113 | className: "leaflet-control-locate-heading",
114 | svg,
115 | w: s2,
116 | h: s2
117 | };
118 | },
119 |
120 | _arrowPoints(radius, width, depth, heading) {
121 | const φ = ((heading - 90) * Math.PI) / 180;
122 | const ux = Math.cos(φ);
123 | const uy = Math.sin(φ);
124 | const vx = -Math.sin(φ);
125 | const vy = Math.cos(φ);
126 | const h = width / 2;
127 |
128 | // Base center on circle
129 | const Cx = radius * ux;
130 | const Cy = radius * uy;
131 |
132 | // Base corners
133 | const B1x = Cx + h * vx;
134 | const B1y = Cy + h * vy;
135 | const B2x = Cx - h * vx;
136 | const B2y = Cy - h * vy;
137 |
138 | // Tip outward
139 | const Tx = Cx + depth * ux;
140 | const Ty = Cy + depth * uy;
141 |
142 | return `M ${B1x},${B1y} L ${B2x},${B2y} L ${Tx},${Ty} Z`;
143 | }
144 | });
145 |
146 | const LocateControl = Control.extend({
147 | options: {
148 | /** Position of the control */
149 | position: "topleft",
150 | /** The layer that the user's location should be drawn on. By default creates a new layer. */
151 | layer: undefined,
152 | /**
153 | * Automatically sets the map view (zoom and pan) to the user's location as it updates.
154 | * While the map is following the user's location, the control is in the `following` state,
155 | * which changes the style of the control and the circle marker.
156 | *
157 | * Possible values:
158 | * - false: never updates the map view when location changes.
159 | * - 'once': set the view when the location is first determined
160 | * - 'always': always updates the map view when location changes.
161 | * The map view follows the user's location.
162 | * - 'untilPan': like 'always', except stops updating the
163 | * view if the user has manually panned the map.
164 | * The map view follows the user's location until she pans.
165 | * - 'untilPanOrZoom': (default) like 'always', except stops updating the
166 | * view if the user has manually panned the map.
167 | * The map view follows the user's location until she pans.
168 | */
169 | setView: "untilPanOrZoom",
170 | /** Keep the current map zoom level when setting the view and only pan. */
171 | keepCurrentZoomLevel: false,
172 | /** 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. */
173 | initialZoomLevel: false,
174 | /**
175 | * This callback can be used to override the viewport tracking
176 | * This function should return a LatLngBounds object.
177 | *
178 | * For example to extend the viewport to ensure that a particular LatLng is visible:
179 | *
180 | * getLocationBounds: function(locationEvent) {
181 | * return locationEvent.bounds.extend([-33.873085, 151.219273]);
182 | * },
183 | */
184 | getLocationBounds(locationEvent) {
185 | return locationEvent.bounds;
186 | },
187 | /** Smooth pan and zoom to the location of the marker. Only works in Leaflet 1.0+. */
188 | flyTo: false,
189 | /**
190 | * The user location can be inside and outside the current view when the user clicks on the
191 | * control that is already active. Both cases can be configures separately.
192 | * Possible values are:
193 | * - 'setView': zoom and pan to the current location
194 | * - 'stop': stop locating and remove the location marker
195 | */
196 | clickBehavior: {
197 | /** What should happen if the user clicks on the control while the location is within the current view. */
198 | inView: "stop",
199 | /** What should happen if the user clicks on the control while the location is outside the current view. */
200 | outOfView: "setView",
201 | /**
202 | * What should happen if the user clicks on the control while the location is within the current view
203 | * and we could be following but are not. Defaults to a special value which inherits from 'inView';
204 | */
205 | inViewNotFollowing: "inView"
206 | },
207 | /**
208 | * If set, save the map bounds just before centering to the user's
209 | * location. When control is disabled, set the view back to the
210 | * bounds that were saved.
211 | */
212 | returnToPrevBounds: false,
213 | /**
214 | * Keep a cache of the location after the user deactivates the control. If set to false, the user has to wait
215 | * until the locate API returns a new location before they see where they are again.
216 | */
217 | cacheLocation: true,
218 | /** If set, a circle that shows the location accuracy is drawn. */
219 | drawCircle: true,
220 | /** If set, the marker at the users' location is drawn. */
221 | drawMarker: true,
222 | /** If set and supported then show the compass heading */
223 | showCompass: true,
224 | /** The class to be used to create the marker. For example L.CircleMarker or L.Marker */
225 | markerClass: LocationMarker,
226 | /** The class us be used to create the compass bearing arrow */
227 | compassClass: CompassMarker,
228 | /** Accuracy circle style properties. NOTE these styles should match the css animations styles */
229 | circleStyle: {
230 | className: "leaflet-control-locate-circle",
231 | color: "#136AEC",
232 | fillColor: "#136AEC",
233 | fillOpacity: 0.15,
234 | weight: 0
235 | },
236 | /** Inner marker style properties. Only works if your marker class supports `setStyle`. */
237 | markerStyle: {
238 | className: "leaflet-control-locate-marker",
239 | color: "#fff",
240 | fillColor: "#2A93EE",
241 | fillOpacity: 1,
242 | weight: 3,
243 | opacity: 1,
244 | radius: 9
245 | },
246 | /** Compass */
247 | compassStyle: {
248 | fillColor: "#2A93EE",
249 | fillOpacity: 1,
250 | weight: 0,
251 | color: "#fff",
252 | opacity: 1,
253 | radius: 9, // How far is the arrow from the center of the marker
254 | width: 9, // Width of the arrow
255 | depth: 6 // Length of the arrow
256 | },
257 | /**
258 | * Changes to accuracy circle and inner marker while following.
259 | * It is only necessary to provide the properties that should change.
260 | */
261 | followCircleStyle: {},
262 | followMarkerStyle: {
263 | // color: '#FFA500',
264 | // fillColor: '#FFB000'
265 | },
266 | followCompassStyle: {},
267 | /** The CSS class for the icon. For example fa-location-arrow or fa-map-marker */
268 | icon: "leaflet-control-locate-location-arrow",
269 | iconLoading: "leaflet-control-locate-spinner",
270 | /** The element to be created for icons. For example span or i */
271 | iconElementTag: "span",
272 | /** The element to be created for the text. For example small or span */
273 | textElementTag: "small",
274 | /** Padding around the accuracy circle. */
275 | circlePadding: [0, 0],
276 | /** Use metric units. */
277 | metric: true,
278 | /**
279 | * This callback can be used in case you would like to override button creation behavior.
280 | * This is useful for DOM manipulation frameworks such as angular etc.
281 | * This function should return an object with HtmlElement for the button (link property) and the icon (icon property).
282 | */
283 | createButtonCallback(container, options) {
284 | const link = DomUtil.create("a", "leaflet-bar-part leaflet-bar-part-single", container);
285 | link.title = options.strings.title;
286 | link.href = "#";
287 | link.setAttribute("role", "button");
288 | const icon = DomUtil.create(options.iconElementTag, options.icon, link);
289 |
290 | if (options.strings.text !== undefined) {
291 | const text = DomUtil.create(options.textElementTag, "leaflet-locate-text", link);
292 | text.textContent = options.strings.text;
293 | link.classList.add("leaflet-locate-text-active");
294 | link.parentNode.style.display = "flex";
295 | if (options.icon.length > 0) {
296 | icon.classList.add("leaflet-locate-icon");
297 | }
298 | }
299 |
300 | return { link, icon };
301 | },
302 | /** This event is called in case of any location error that is not a time out error. */
303 | onLocationError(err, control) {
304 | alert(err.message);
305 | },
306 | /**
307 | * This event is called when the user's location is outside the bounds set on the map.
308 | * The event is called repeatedly when the location changes.
309 | */
310 | onLocationOutsideMapBounds(control) {
311 | control.stop();
312 | alert(control.options.strings.outsideMapBoundsMsg);
313 | },
314 | /** Display a pop-up when the user click on the inner marker. */
315 | showPopup: true,
316 | strings: {
317 | title: "Show me where I am",
318 | metersUnit: "meters",
319 | feetUnit: "feet",
320 | popup: "You are within {distance} {unit} from this point",
321 | outsideMapBoundsMsg: "You seem located outside the boundaries of the map"
322 | },
323 | /** The default options passed to leaflets locate method. */
324 | locateOptions: {
325 | maxZoom: Infinity,
326 | watch: true, // if you overwrite this, visualization cannot be updated
327 | setView: false // have to set this to false because we have to
328 | // do setView manually
329 | }
330 | },
331 |
332 | initialize(options) {
333 | // set default options if nothing is set (merge one step deep)
334 | for (const i in options) {
335 | if (typeof this.options[i] === "object") {
336 | extend(this.options[i], options[i]);
337 | } else {
338 | this.options[i] = options[i];
339 | }
340 | }
341 |
342 | // extend the follow marker style and circle from the normal style
343 | this.options.followMarkerStyle = extend({}, this.options.markerStyle, this.options.followMarkerStyle);
344 | this.options.followCircleStyle = extend({}, this.options.circleStyle, this.options.followCircleStyle);
345 | this.options.followCompassStyle = extend({}, this.options.compassStyle, this.options.followCompassStyle);
346 | },
347 |
348 | /**
349 | * Add control to map. Returns the container for the control.
350 | */
351 | onAdd(map) {
352 | const container = DomUtil.create("div", "leaflet-control-locate leaflet-bar leaflet-control");
353 | this._container = container;
354 | this._map = map;
355 | this._layer = this.options.layer || new LayerGroup();
356 | this._layer.addTo(map);
357 | this._event = undefined;
358 | this._compassHeading = null;
359 | this._prevBounds = null;
360 |
361 | const linkAndIcon = this.options.createButtonCallback(container, this.options);
362 | this._link = linkAndIcon.link;
363 | this._icon = linkAndIcon.icon;
364 |
365 | DomEvent.on(
366 | this._link,
367 | "click",
368 | function (ev) {
369 | DomEvent.stopPropagation(ev);
370 | DomEvent.preventDefault(ev);
371 | this._onClick();
372 | },
373 | this
374 | ).on(this._link, "dblclick", DomEvent.stopPropagation);
375 |
376 | this._resetVariables();
377 |
378 | this._map.on("unload", this._unload, this);
379 |
380 | return container;
381 | },
382 |
383 | /**
384 | * This method is called when the user clicks on the control.
385 | */
386 | _onClick() {
387 | this._justClicked = true;
388 | const wasFollowing = this._isFollowing();
389 | this._userPanned = false;
390 | this._userZoomed = false;
391 |
392 | if (this._active && !this._event) {
393 | // click while requesting
394 | this.stop();
395 | } else if (this._active) {
396 | const behaviors = this.options.clickBehavior;
397 | let behavior = behaviors.outOfView;
398 | if (this._map.getBounds().contains(this._event.latlng)) {
399 | behavior = wasFollowing ? behaviors.inView : behaviors.inViewNotFollowing;
400 | }
401 |
402 | // Allow inheriting from another behavior
403 | if (behaviors[behavior]) {
404 | behavior = behaviors[behavior];
405 | }
406 |
407 | switch (behavior) {
408 | case "setView":
409 | this.setView();
410 | break;
411 | case "stop":
412 | this.stop();
413 | if (this.options.returnToPrevBounds) {
414 | const f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds;
415 | f.bind(this._map)(this._prevBounds);
416 | }
417 | break;
418 | }
419 | } else {
420 | if (this.options.returnToPrevBounds) {
421 | this._prevBounds = this._map.getBounds();
422 | }
423 | this.start();
424 | }
425 |
426 | this._updateContainerStyle();
427 | },
428 |
429 | /**
430 | * Starts the plugin:
431 | * - activates the engine
432 | * - draws the marker (if coordinates available)
433 | */
434 | start() {
435 | this._activate();
436 |
437 | if (this._event) {
438 | this._drawMarker(this._map);
439 |
440 | // if we already have a location but the user clicked on the control
441 | if (this.options.setView) {
442 | this.setView();
443 | }
444 | }
445 | this._updateContainerStyle();
446 | },
447 |
448 | /**
449 | * Stops the plugin:
450 | * - deactivates the engine
451 | * - reinitializes the button
452 | * - removes the marker
453 | */
454 | stop() {
455 | this._deactivate();
456 |
457 | this._cleanClasses();
458 | this._resetVariables();
459 |
460 | this._removeMarker();
461 | },
462 |
463 | /**
464 | * Keep the control active but stop following the location
465 | */
466 | stopFollowing() {
467 | this._userPanned = true;
468 | this._updateContainerStyle();
469 | this._drawMarker();
470 | },
471 |
472 | /**
473 | * This method launches the location engine.
474 | * It is called before the marker is updated,
475 | * event if it does not mean that the event will be ready.
476 | *
477 | * Override it if you want to add more functionalities.
478 | * It should set the this._active to true and do nothing if
479 | * this._active is true.
480 | */
481 | _activate() {
482 | if (this._active || !this._map) {
483 | return;
484 | }
485 |
486 | this._map.locate(this.options.locateOptions);
487 | this._map.fire("locateactivate", this);
488 | this._active = true;
489 |
490 | // bind event listeners
491 | this._map.on("locationfound", this._onLocationFound, this);
492 | this._map.on("locationerror", this._onLocationError, this);
493 | this._map.on("dragstart", this._onDrag, this);
494 | this._map.on("zoomstart", this._onZoom, this);
495 | this._map.on("zoomend", this._onZoomEnd, this);
496 | if (this.options.showCompass) {
497 | const oriAbs = "ondeviceorientationabsolute" in window;
498 | if (oriAbs || "ondeviceorientation" in window) {
499 | const _this = this;
500 | const deviceorientation = function () {
501 | DomEvent.on(window, oriAbs ? "deviceorientationabsolute" : "deviceorientation", _this._onDeviceOrientation, _this);
502 | };
503 | if (DeviceOrientationEvent && typeof DeviceOrientationEvent.requestPermission === "function") {
504 | DeviceOrientationEvent.requestPermission().then(function (permissionState) {
505 | if (permissionState === "granted") {
506 | deviceorientation();
507 | }
508 | });
509 | } else {
510 | deviceorientation();
511 | }
512 | }
513 | }
514 | },
515 |
516 | /**
517 | * Called to stop the location engine.
518 | *
519 | * Override it to shutdown any functionalities you added on start.
520 | */
521 | _deactivate() {
522 | if (!this._active || !this._map) {
523 | return;
524 | }
525 |
526 | this._map.stopLocate();
527 | this._map.fire("locatedeactivate", this);
528 | this._active = false;
529 |
530 | if (!this.options.cacheLocation) {
531 | this._event = undefined;
532 | }
533 |
534 | // unbind event listeners
535 | this._map.off("locationfound", this._onLocationFound, this);
536 | this._map.off("locationerror", this._onLocationError, this);
537 | this._map.off("dragstart", this._onDrag, this);
538 | this._map.off("zoomstart", this._onZoom, this);
539 | this._map.off("zoomend", this._onZoomEnd, this);
540 | if (this.options.showCompass) {
541 | this._compassHeading = null;
542 | if ("ondeviceorientationabsolute" in window) {
543 | DomEvent.off(window, "deviceorientationabsolute", this._onDeviceOrientation, this);
544 | } else if ("ondeviceorientation" in window) {
545 | DomEvent.off(window, "deviceorientation", this._onDeviceOrientation, this);
546 | }
547 | }
548 | },
549 |
550 | /**
551 | * Zoom (unless we should keep the zoom level) and an to the current view.
552 | */
553 | setView() {
554 | this._drawMarker();
555 | if (this._isOutsideMapBounds()) {
556 | this._event = undefined; // clear the current location so we can get back into the bounds
557 | this.options.onLocationOutsideMapBounds(this);
558 | } else {
559 | if (this._justClicked && this.options.initialZoomLevel !== false) {
560 | let f = this.options.flyTo ? this._map.flyTo : this._map.setView;
561 | f.bind(this._map)([this._event.latitude, this._event.longitude], this.options.initialZoomLevel);
562 | } else if (this.options.keepCurrentZoomLevel) {
563 | let f = this.options.flyTo ? this._map.flyTo : this._map.panTo;
564 | f.bind(this._map)([this._event.latitude, this._event.longitude]);
565 | } else {
566 | let f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds;
567 | // Ignore zoom events while setting the viewport as these would stop following
568 | this._ignoreEvent = true;
569 | f.bind(this._map)(this.options.getLocationBounds(this._event), {
570 | padding: this.options.circlePadding,
571 | maxZoom: this.options.initialZoomLevel || this.options.locateOptions.maxZoom
572 | });
573 | Util.requestAnimFrame(function () {
574 | // Wait until after the next animFrame because the flyTo can be async
575 | this._ignoreEvent = false;
576 | }, this);
577 | }
578 | }
579 | },
580 |
581 | /**
582 | *
583 | */
584 | _drawCompass() {
585 | if (!this._event) {
586 | return;
587 | }
588 |
589 | const latlng = this._event.latlng;
590 |
591 | if (this.options.showCompass && latlng && this._compassHeading !== null) {
592 | const cStyle = this._isFollowing() ? this.options.followCompassStyle : this.options.compassStyle;
593 | if (!this._compass) {
594 | this._compass = new this.options.compassClass(latlng, this._compassHeading, cStyle).addTo(this._layer);
595 | } else {
596 | this._compass.setLatLng(latlng);
597 | this._compass.setHeading(this._compassHeading);
598 | // If the compassClass can be updated with setStyle, update it.
599 | if (this._compass.setStyle) {
600 | this._compass.setStyle(cStyle);
601 | }
602 | }
603 | //
604 | }
605 | if (this._compass && (!this.options.showCompass || this._compassHeading === null)) {
606 | this._compass.removeFrom(this._layer);
607 | this._compass = null;
608 | }
609 | },
610 |
611 | /**
612 | * Draw the marker and accuracy circle on the map.
613 | *
614 | * Uses the event retrieved from onLocationFound from the map.
615 | */
616 | _drawMarker() {
617 | if (this._event.accuracy === undefined) {
618 | this._event.accuracy = 0;
619 | }
620 |
621 | const radius = this._event.accuracy;
622 | const latlng = this._event.latlng;
623 |
624 | // circle with the radius of the location's accuracy
625 | if (this.options.drawCircle) {
626 | const style = this._isFollowing() ? this.options.followCircleStyle : this.options.circleStyle;
627 |
628 | if (!this._circle) {
629 | this._circle = circle(latlng, radius, style).addTo(this._layer);
630 | } else {
631 | this._circle.setLatLng(latlng).setRadius(radius).setStyle(style);
632 | }
633 | }
634 |
635 | let distance;
636 | let unit;
637 | if (this.options.metric) {
638 | distance = radius.toFixed(0);
639 | unit = this.options.strings.metersUnit;
640 | } else {
641 | distance = (radius * 3.2808399).toFixed(0);
642 | unit = this.options.strings.feetUnit;
643 | }
644 |
645 | // small inner marker
646 | if (this.options.drawMarker) {
647 | const mStyle = this._isFollowing() ? this.options.followMarkerStyle : this.options.markerStyle;
648 | if (!this._marker) {
649 | this._marker = new this.options.markerClass(latlng, mStyle).addTo(this._layer);
650 | } else {
651 | this._marker.setLatLng(latlng);
652 | // If the markerClass can be updated with setStyle, update it.
653 | if (this._marker.setStyle) {
654 | this._marker.setStyle(mStyle);
655 | }
656 | }
657 | }
658 |
659 | this._drawCompass();
660 |
661 | const t = this.options.strings.popup;
662 | function getPopupText() {
663 | if (typeof t === "string") {
664 | return Util.template(t, { distance, unit });
665 | } else if (typeof t === "function") {
666 | return t({ distance, unit });
667 | } else {
668 | return t;
669 | }
670 | }
671 | if (this.options.showPopup && t && this._marker) {
672 | this._marker.bindPopup(getPopupText())._popup.setLatLng(latlng);
673 | }
674 | if (this.options.showPopup && t && this._compass) {
675 | this._compass.bindPopup(getPopupText())._popup.setLatLng(latlng);
676 | }
677 | },
678 |
679 | /**
680 | * Remove the marker from map.
681 | */
682 | _removeMarker() {
683 | this._layer.clearLayers();
684 | this._marker = undefined;
685 | this._circle = undefined;
686 | },
687 |
688 | /**
689 | * Unload the plugin and all event listeners.
690 | * Kind of the opposite of onAdd.
691 | */
692 | _unload() {
693 | this.stop();
694 | // May become undefined during HMR
695 | if (this._map) {
696 | this._map.off("unload", this._unload, this);
697 | }
698 | },
699 |
700 | /**
701 | * Sets the compass heading
702 | */
703 | _setCompassHeading(angle) {
704 | if (!isNaN(parseFloat(angle)) && isFinite(angle)) {
705 | angle = Math.round(angle);
706 |
707 | this._compassHeading = angle;
708 | Util.requestAnimFrame(this._drawCompass, this);
709 | } else {
710 | this._compassHeading = null;
711 | }
712 | },
713 |
714 | /**
715 | * If the compass fails calibration just fail safely and remove the compass
716 | */
717 | _onCompassNeedsCalibration() {
718 | this._setCompassHeading();
719 | },
720 |
721 | /**
722 | * Process and normalise compass events
723 | */
724 | _onDeviceOrientation(e) {
725 | if (!this._active) {
726 | return;
727 | }
728 |
729 | if (e.webkitCompassHeading) {
730 | // iOS
731 | this._setCompassHeading(e.webkitCompassHeading);
732 | } else if (e.absolute && e.alpha) {
733 | // Android
734 | this._setCompassHeading(360 - e.alpha);
735 | }
736 | },
737 |
738 | /**
739 | * Calls deactivate and dispatches an error.
740 | */
741 | _onLocationError(err) {
742 | // ignore time out error if the location is watched
743 | if (err.code == 3 && this.options.locateOptions.watch) {
744 | return;
745 | }
746 |
747 | this.stop();
748 | this.options.onLocationError(err, this);
749 | },
750 |
751 | /**
752 | * Stores the received event and updates the marker.
753 | */
754 | _onLocationFound(e) {
755 | // no need to do anything if the location has not changed
756 | if (this._event && this._event.latlng.lat === e.latlng.lat && this._event.latlng.lng === e.latlng.lng && this._event.accuracy === e.accuracy) {
757 | return;
758 | }
759 |
760 | if (!this._active) {
761 | // we may have a stray event
762 | return;
763 | }
764 |
765 | this._event = e;
766 |
767 | this._drawMarker();
768 | this._updateContainerStyle();
769 |
770 | switch (this.options.setView) {
771 | case "once":
772 | if (this._justClicked) {
773 | this.setView();
774 | }
775 | break;
776 | case "untilPan":
777 | if (!this._userPanned) {
778 | this.setView();
779 | }
780 | break;
781 | case "untilPanOrZoom":
782 | if (!this._userPanned && !this._userZoomed) {
783 | this.setView();
784 | }
785 | break;
786 | case "always":
787 | this.setView();
788 | break;
789 | }
790 |
791 | this._justClicked = false;
792 | },
793 |
794 | /**
795 | * When the user drags. Need a separate event so we can bind and unbind event listeners.
796 | */
797 | _onDrag() {
798 | // only react to drags once we have a location
799 | if (this._event && !this._ignoreEvent) {
800 | this._userPanned = true;
801 | this._updateContainerStyle();
802 | this._drawMarker();
803 | }
804 | },
805 |
806 | /**
807 | * When the user zooms. Need a separate event so we can bind and unbind event listeners.
808 | */
809 | _onZoom() {
810 | // only react to drags once we have a location
811 | if (this._event && !this._ignoreEvent) {
812 | this._userZoomed = true;
813 | this._updateContainerStyle();
814 | this._drawMarker();
815 | }
816 | },
817 |
818 | /**
819 | * After a zoom ends update the compass and handle sideways zooms
820 | */
821 | _onZoomEnd() {
822 | if (this._event) {
823 | this._drawCompass();
824 | }
825 |
826 | if (this._event && !this._ignoreEvent) {
827 | // If we have zoomed in and out and ended up sideways treat it as a pan
828 | if (this._marker && !this._map.getBounds().pad(-0.3).contains(this._marker.getLatLng())) {
829 | this._userPanned = true;
830 | this._updateContainerStyle();
831 | this._drawMarker();
832 | }
833 | }
834 | },
835 |
836 | /**
837 | * Compute whether the map is following the user location with pan and zoom.
838 | */
839 | _isFollowing() {
840 | if (!this._active) {
841 | return false;
842 | }
843 |
844 | if (this.options.setView === "always") {
845 | return true;
846 | } else if (this.options.setView === "untilPan") {
847 | return !this._userPanned;
848 | } else if (this.options.setView === "untilPanOrZoom") {
849 | return !this._userPanned && !this._userZoomed;
850 | }
851 | },
852 |
853 | /**
854 | * Check if location is in map bounds
855 | */
856 | _isOutsideMapBounds() {
857 | if (this._event === undefined) {
858 | return false;
859 | }
860 | return this._map.options.maxBounds && !this._map.options.maxBounds.contains(this._event.latlng);
861 | },
862 |
863 | /**
864 | * Toggles button class between following and active.
865 | */
866 | _updateContainerStyle() {
867 | if (!this._container) {
868 | return;
869 | }
870 |
871 | if (this._active && !this._event) {
872 | // active but don't have a location yet
873 | this._setClasses("requesting");
874 | } else if (this._isFollowing()) {
875 | this._setClasses("following");
876 | } else if (this._active) {
877 | this._setClasses("active");
878 | } else {
879 | this._cleanClasses();
880 | }
881 | },
882 |
883 | /**
884 | * Sets the CSS classes for the state.
885 | */
886 | _setClasses(state) {
887 | if (state == "requesting") {
888 | removeClasses(this._container, "active following");
889 | addClasses(this._container, "requesting");
890 |
891 | removeClasses(this._icon, this.options.icon);
892 | addClasses(this._icon, this.options.iconLoading);
893 | } else if (state == "active") {
894 | removeClasses(this._container, "requesting following");
895 | addClasses(this._container, "active");
896 |
897 | removeClasses(this._icon, this.options.iconLoading);
898 | addClasses(this._icon, this.options.icon);
899 | } else if (state == "following") {
900 | removeClasses(this._container, "requesting");
901 | addClasses(this._container, "active following");
902 |
903 | removeClasses(this._icon, this.options.iconLoading);
904 | addClasses(this._icon, this.options.icon);
905 | }
906 | },
907 |
908 | /**
909 | * Removes all classes from button.
910 | */
911 | _cleanClasses() {
912 | DomUtil.removeClass(this._container, "requesting");
913 | DomUtil.removeClass(this._container, "active");
914 | DomUtil.removeClass(this._container, "following");
915 |
916 | removeClasses(this._icon, this.options.iconLoading);
917 | addClasses(this._icon, this.options.icon);
918 | },
919 |
920 | /**
921 | * Reinitializes state variables.
922 | */
923 | _resetVariables() {
924 | // whether locate is active or not
925 | this._active = false;
926 |
927 | // true if the control was clicked for the first time
928 | // we need this so we can pan and zoom once we have the location
929 | this._justClicked = false;
930 |
931 | // true if the user has panned the map after clicking the control
932 | this._userPanned = false;
933 |
934 | // true if the user has zoomed the map after clicking the control
935 | this._userZoomed = false;
936 | }
937 | });
938 |
939 | function locate(options) {
940 | return new LocateControl(options);
941 | }
942 |
943 | export { CompassMarker, LocateControl, LocationMarker, locate };
944 |
--------------------------------------------------------------------------------
/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 s = r + options.weight + options.depth;
104 | const s2 = s * 2;
105 |
106 | const path = this._arrowPoints(r, options.width, options.depth, this._heading);
107 |
108 | const svg =
109 | `` +
110 | ` `;
111 | return {
112 | className: "leaflet-control-locate-heading",
113 | svg,
114 | w: s2,
115 | h: s2
116 | };
117 | },
118 |
119 | _arrowPoints(radius, width, depth, heading) {
120 | const φ = ((heading - 90) * Math.PI) / 180;
121 | const ux = Math.cos(φ);
122 | const uy = Math.sin(φ);
123 | const vx = -Math.sin(φ);
124 | const vy = Math.cos(φ);
125 | const h = width / 2;
126 |
127 | // Base center on circle
128 | const Cx = radius * ux;
129 | const Cy = radius * uy;
130 |
131 | // Base corners
132 | const B1x = Cx + h * vx;
133 | const B1y = Cy + h * vy;
134 | const B2x = Cx - h * vx;
135 | const B2y = Cy - h * vy;
136 |
137 | // Tip outward
138 | const Tx = Cx + depth * ux;
139 | const Ty = Cy + depth * uy;
140 |
141 | return `M ${B1x},${B1y} L ${B2x},${B2y} L ${Tx},${Ty} Z`;
142 | }
143 | });
144 |
145 | const LocateControl = Control.extend({
146 | options: {
147 | /** Position of the control */
148 | position: "topleft",
149 | /** The layer that the user's location should be drawn on. By default creates a new layer. */
150 | layer: undefined,
151 | /**
152 | * Automatically sets the map view (zoom and pan) to the user's location as it updates.
153 | * While the map is following the user's location, the control is in the `following` state,
154 | * which changes the style of the control and the circle marker.
155 | *
156 | * Possible values:
157 | * - false: never updates the map view when location changes.
158 | * - 'once': set the view when the location is first determined
159 | * - 'always': always updates the map view when location changes.
160 | * The map view follows the user's location.
161 | * - 'untilPan': like 'always', except stops updating the
162 | * view if the user has manually panned the map.
163 | * The map view follows the user's location until she pans.
164 | * - 'untilPanOrZoom': (default) like 'always', except stops updating the
165 | * view if the user has manually panned the map.
166 | * The map view follows the user's location until she pans.
167 | */
168 | setView: "untilPanOrZoom",
169 | /** Keep the current map zoom level when setting the view and only pan. */
170 | keepCurrentZoomLevel: false,
171 | /** 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. */
172 | initialZoomLevel: false,
173 | /**
174 | * This callback can be used to override the viewport tracking
175 | * This function should return a LatLngBounds object.
176 | *
177 | * For example to extend the viewport to ensure that a particular LatLng is visible:
178 | *
179 | * getLocationBounds: function(locationEvent) {
180 | * return locationEvent.bounds.extend([-33.873085, 151.219273]);
181 | * },
182 | */
183 | getLocationBounds(locationEvent) {
184 | return locationEvent.bounds;
185 | },
186 | /** Smooth pan and zoom to the location of the marker. Only works in Leaflet 1.0+. */
187 | flyTo: false,
188 | /**
189 | * The user location can be inside and outside the current view when the user clicks on the
190 | * control that is already active. Both cases can be configures separately.
191 | * Possible values are:
192 | * - 'setView': zoom and pan to the current location
193 | * - 'stop': stop locating and remove the location marker
194 | */
195 | clickBehavior: {
196 | /** What should happen if the user clicks on the control while the location is within the current view. */
197 | inView: "stop",
198 | /** What should happen if the user clicks on the control while the location is outside the current view. */
199 | outOfView: "setView",
200 | /**
201 | * What should happen if the user clicks on the control while the location is within the current view
202 | * and we could be following but are not. Defaults to a special value which inherits from 'inView';
203 | */
204 | inViewNotFollowing: "inView"
205 | },
206 | /**
207 | * If set, save the map bounds just before centering to the user's
208 | * location. When control is disabled, set the view back to the
209 | * bounds that were saved.
210 | */
211 | returnToPrevBounds: false,
212 | /**
213 | * Keep a cache of the location after the user deactivates the control. If set to false, the user has to wait
214 | * until the locate API returns a new location before they see where they are again.
215 | */
216 | cacheLocation: true,
217 | /** If set, a circle that shows the location accuracy is drawn. */
218 | drawCircle: true,
219 | /** If set, the marker at the users' location is drawn. */
220 | drawMarker: true,
221 | /** If set and supported then show the compass heading */
222 | showCompass: true,
223 | /** The class to be used to create the marker. For example L.CircleMarker or L.Marker */
224 | markerClass: LocationMarker,
225 | /** The class us be used to create the compass bearing arrow */
226 | compassClass: CompassMarker,
227 | /** Accuracy circle style properties. NOTE these styles should match the css animations styles */
228 | circleStyle: {
229 | className: "leaflet-control-locate-circle",
230 | color: "#136AEC",
231 | fillColor: "#136AEC",
232 | fillOpacity: 0.15,
233 | weight: 0
234 | },
235 | /** Inner marker style properties. Only works if your marker class supports `setStyle`. */
236 | markerStyle: {
237 | className: "leaflet-control-locate-marker",
238 | color: "#fff",
239 | fillColor: "#2A93EE",
240 | fillOpacity: 1,
241 | weight: 3,
242 | opacity: 1,
243 | radius: 9
244 | },
245 | /** Compass */
246 | compassStyle: {
247 | fillColor: "#2A93EE",
248 | fillOpacity: 1,
249 | weight: 0,
250 | color: "#fff",
251 | opacity: 1,
252 | radius: 9, // How far is the arrow from the center of the marker
253 | width: 9, // Width of the arrow
254 | depth: 6 // Length of the arrow
255 | },
256 | /**
257 | * Changes to accuracy circle and inner marker while following.
258 | * It is only necessary to provide the properties that should change.
259 | */
260 | followCircleStyle: {},
261 | followMarkerStyle: {
262 | // color: '#FFA500',
263 | // fillColor: '#FFB000'
264 | },
265 | followCompassStyle: {},
266 | /** The CSS class for the icon. For example fa-location-arrow or fa-map-marker */
267 | icon: "leaflet-control-locate-location-arrow",
268 | iconLoading: "leaflet-control-locate-spinner",
269 | /** The element to be created for icons. For example span or i */
270 | iconElementTag: "span",
271 | /** The element to be created for the text. For example small or span */
272 | textElementTag: "small",
273 | /** Padding around the accuracy circle. */
274 | circlePadding: [0, 0],
275 | /** Use metric units. */
276 | metric: true,
277 | /**
278 | * This callback can be used in case you would like to override button creation behavior.
279 | * This is useful for DOM manipulation frameworks such as angular etc.
280 | * This function should return an object with HtmlElement for the button (link property) and the icon (icon property).
281 | */
282 | createButtonCallback(container, options) {
283 | const link = DomUtil.create("a", "leaflet-bar-part leaflet-bar-part-single", container);
284 | link.title = options.strings.title;
285 | link.href = "#";
286 | link.setAttribute("role", "button");
287 | const icon = DomUtil.create(options.iconElementTag, options.icon, link);
288 |
289 | if (options.strings.text !== undefined) {
290 | const text = DomUtil.create(options.textElementTag, "leaflet-locate-text", link);
291 | text.textContent = options.strings.text;
292 | link.classList.add("leaflet-locate-text-active");
293 | link.parentNode.style.display = "flex";
294 | if (options.icon.length > 0) {
295 | icon.classList.add("leaflet-locate-icon");
296 | }
297 | }
298 |
299 | return { link, icon };
300 | },
301 | /** This event is called in case of any location error that is not a time out error. */
302 | onLocationError(err, control) {
303 | alert(err.message);
304 | },
305 | /**
306 | * This event is called when the user's location is outside the bounds set on the map.
307 | * The event is called repeatedly when the location changes.
308 | */
309 | onLocationOutsideMapBounds(control) {
310 | control.stop();
311 | alert(control.options.strings.outsideMapBoundsMsg);
312 | },
313 | /** Display a pop-up when the user click on the inner marker. */
314 | showPopup: true,
315 | strings: {
316 | title: "Show me where I am",
317 | metersUnit: "meters",
318 | feetUnit: "feet",
319 | popup: "You are within {distance} {unit} from this point",
320 | outsideMapBoundsMsg: "You seem located outside the boundaries of the map"
321 | },
322 | /** The default options passed to leaflets locate method. */
323 | locateOptions: {
324 | maxZoom: Infinity,
325 | watch: true, // if you overwrite this, visualization cannot be updated
326 | setView: false // have to set this to false because we have to
327 | // do setView manually
328 | }
329 | },
330 |
331 | initialize(options) {
332 | // set default options if nothing is set (merge one step deep)
333 | for (const i in options) {
334 | if (typeof this.options[i] === "object") {
335 | extend(this.options[i], options[i]);
336 | } else {
337 | this.options[i] = options[i];
338 | }
339 | }
340 |
341 | // extend the follow marker style and circle from the normal style
342 | this.options.followMarkerStyle = extend({}, this.options.markerStyle, this.options.followMarkerStyle);
343 | this.options.followCircleStyle = extend({}, this.options.circleStyle, this.options.followCircleStyle);
344 | this.options.followCompassStyle = extend({}, this.options.compassStyle, this.options.followCompassStyle);
345 | },
346 |
347 | /**
348 | * Add control to map. Returns the container for the control.
349 | */
350 | onAdd(map) {
351 | const container = DomUtil.create("div", "leaflet-control-locate leaflet-bar leaflet-control");
352 | this._container = container;
353 | this._map = map;
354 | this._layer = this.options.layer || new LayerGroup();
355 | this._layer.addTo(map);
356 | this._event = undefined;
357 | this._compassHeading = null;
358 | this._prevBounds = null;
359 |
360 | const linkAndIcon = this.options.createButtonCallback(container, this.options);
361 | this._link = linkAndIcon.link;
362 | this._icon = linkAndIcon.icon;
363 |
364 | DomEvent.on(
365 | this._link,
366 | "click",
367 | function (ev) {
368 | DomEvent.stopPropagation(ev);
369 | DomEvent.preventDefault(ev);
370 | this._onClick();
371 | },
372 | this
373 | ).on(this._link, "dblclick", DomEvent.stopPropagation);
374 |
375 | this._resetVariables();
376 |
377 | this._map.on("unload", this._unload, this);
378 |
379 | return container;
380 | },
381 |
382 | /**
383 | * This method is called when the user clicks on the control.
384 | */
385 | _onClick() {
386 | this._justClicked = true;
387 | const wasFollowing = this._isFollowing();
388 | this._userPanned = false;
389 | this._userZoomed = false;
390 |
391 | if (this._active && !this._event) {
392 | // click while requesting
393 | this.stop();
394 | } else if (this._active) {
395 | const behaviors = this.options.clickBehavior;
396 | let behavior = behaviors.outOfView;
397 | if (this._map.getBounds().contains(this._event.latlng)) {
398 | behavior = wasFollowing ? behaviors.inView : behaviors.inViewNotFollowing;
399 | }
400 |
401 | // Allow inheriting from another behavior
402 | if (behaviors[behavior]) {
403 | behavior = behaviors[behavior];
404 | }
405 |
406 | switch (behavior) {
407 | case "setView":
408 | this.setView();
409 | break;
410 | case "stop":
411 | this.stop();
412 | if (this.options.returnToPrevBounds) {
413 | const f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds;
414 | f.bind(this._map)(this._prevBounds);
415 | }
416 | break;
417 | }
418 | } else {
419 | if (this.options.returnToPrevBounds) {
420 | this._prevBounds = this._map.getBounds();
421 | }
422 | this.start();
423 | }
424 |
425 | this._updateContainerStyle();
426 | },
427 |
428 | /**
429 | * Starts the plugin:
430 | * - activates the engine
431 | * - draws the marker (if coordinates available)
432 | */
433 | start() {
434 | this._activate();
435 |
436 | if (this._event) {
437 | this._drawMarker(this._map);
438 |
439 | // if we already have a location but the user clicked on the control
440 | if (this.options.setView) {
441 | this.setView();
442 | }
443 | }
444 | this._updateContainerStyle();
445 | },
446 |
447 | /**
448 | * Stops the plugin:
449 | * - deactivates the engine
450 | * - reinitializes the button
451 | * - removes the marker
452 | */
453 | stop() {
454 | this._deactivate();
455 |
456 | this._cleanClasses();
457 | this._resetVariables();
458 |
459 | this._removeMarker();
460 | },
461 |
462 | /**
463 | * Keep the control active but stop following the location
464 | */
465 | stopFollowing() {
466 | this._userPanned = true;
467 | this._updateContainerStyle();
468 | this._drawMarker();
469 | },
470 |
471 | /**
472 | * This method launches the location engine.
473 | * It is called before the marker is updated,
474 | * event if it does not mean that the event will be ready.
475 | *
476 | * Override it if you want to add more functionalities.
477 | * It should set the this._active to true and do nothing if
478 | * this._active is true.
479 | */
480 | _activate() {
481 | if (this._active || !this._map) {
482 | return;
483 | }
484 |
485 | this._map.locate(this.options.locateOptions);
486 | this._map.fire("locateactivate", this);
487 | this._active = true;
488 |
489 | // bind event listeners
490 | this._map.on("locationfound", this._onLocationFound, this);
491 | this._map.on("locationerror", this._onLocationError, this);
492 | this._map.on("dragstart", this._onDrag, this);
493 | this._map.on("zoomstart", this._onZoom, this);
494 | this._map.on("zoomend", this._onZoomEnd, this);
495 | if (this.options.showCompass) {
496 | const oriAbs = "ondeviceorientationabsolute" in window;
497 | if (oriAbs || "ondeviceorientation" in window) {
498 | const _this = this;
499 | const deviceorientation = function () {
500 | DomEvent.on(window, oriAbs ? "deviceorientationabsolute" : "deviceorientation", _this._onDeviceOrientation, _this);
501 | };
502 | if (DeviceOrientationEvent && typeof DeviceOrientationEvent.requestPermission === "function") {
503 | DeviceOrientationEvent.requestPermission().then(function (permissionState) {
504 | if (permissionState === "granted") {
505 | deviceorientation();
506 | }
507 | });
508 | } else {
509 | deviceorientation();
510 | }
511 | }
512 | }
513 | },
514 |
515 | /**
516 | * Called to stop the location engine.
517 | *
518 | * Override it to shutdown any functionalities you added on start.
519 | */
520 | _deactivate() {
521 | if (!this._active || !this._map) {
522 | return;
523 | }
524 |
525 | this._map.stopLocate();
526 | this._map.fire("locatedeactivate", this);
527 | this._active = false;
528 |
529 | if (!this.options.cacheLocation) {
530 | this._event = undefined;
531 | }
532 |
533 | // unbind event listeners
534 | this._map.off("locationfound", this._onLocationFound, this);
535 | this._map.off("locationerror", this._onLocationError, this);
536 | this._map.off("dragstart", this._onDrag, this);
537 | this._map.off("zoomstart", this._onZoom, this);
538 | this._map.off("zoomend", this._onZoomEnd, this);
539 | if (this.options.showCompass) {
540 | this._compassHeading = null;
541 | if ("ondeviceorientationabsolute" in window) {
542 | DomEvent.off(window, "deviceorientationabsolute", this._onDeviceOrientation, this);
543 | } else if ("ondeviceorientation" in window) {
544 | DomEvent.off(window, "deviceorientation", this._onDeviceOrientation, this);
545 | }
546 | }
547 | },
548 |
549 | /**
550 | * Zoom (unless we should keep the zoom level) and an to the current view.
551 | */
552 | setView() {
553 | this._drawMarker();
554 | if (this._isOutsideMapBounds()) {
555 | this._event = undefined; // clear the current location so we can get back into the bounds
556 | this.options.onLocationOutsideMapBounds(this);
557 | } else {
558 | if (this._justClicked && this.options.initialZoomLevel !== false) {
559 | let f = this.options.flyTo ? this._map.flyTo : this._map.setView;
560 | f.bind(this._map)([this._event.latitude, this._event.longitude], this.options.initialZoomLevel);
561 | } else if (this.options.keepCurrentZoomLevel) {
562 | let f = this.options.flyTo ? this._map.flyTo : this._map.panTo;
563 | f.bind(this._map)([this._event.latitude, this._event.longitude]);
564 | } else {
565 | let f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds;
566 | // Ignore zoom events while setting the viewport as these would stop following
567 | this._ignoreEvent = true;
568 | f.bind(this._map)(this.options.getLocationBounds(this._event), {
569 | padding: this.options.circlePadding,
570 | maxZoom: this.options.initialZoomLevel || this.options.locateOptions.maxZoom
571 | });
572 | LeafletUtil.requestAnimFrame(function () {
573 | // Wait until after the next animFrame because the flyTo can be async
574 | this._ignoreEvent = false;
575 | }, this);
576 | }
577 | }
578 | },
579 |
580 | /**
581 | *
582 | */
583 | _drawCompass() {
584 | if (!this._event) {
585 | return;
586 | }
587 |
588 | const latlng = this._event.latlng;
589 |
590 | if (this.options.showCompass && latlng && this._compassHeading !== null) {
591 | const cStyle = this._isFollowing() ? this.options.followCompassStyle : this.options.compassStyle;
592 | if (!this._compass) {
593 | this._compass = new this.options.compassClass(latlng, this._compassHeading, cStyle).addTo(this._layer);
594 | } else {
595 | this._compass.setLatLng(latlng);
596 | this._compass.setHeading(this._compassHeading);
597 | // If the compassClass can be updated with setStyle, update it.
598 | if (this._compass.setStyle) {
599 | this._compass.setStyle(cStyle);
600 | }
601 | }
602 | //
603 | }
604 | if (this._compass && (!this.options.showCompass || this._compassHeading === null)) {
605 | this._compass.removeFrom(this._layer);
606 | this._compass = null;
607 | }
608 | },
609 |
610 | /**
611 | * Draw the marker and accuracy circle on the map.
612 | *
613 | * Uses the event retrieved from onLocationFound from the map.
614 | */
615 | _drawMarker() {
616 | if (this._event.accuracy === undefined) {
617 | this._event.accuracy = 0;
618 | }
619 |
620 | const radius = this._event.accuracy;
621 | const latlng = this._event.latlng;
622 |
623 | // circle with the radius of the location's accuracy
624 | if (this.options.drawCircle) {
625 | const style = this._isFollowing() ? this.options.followCircleStyle : this.options.circleStyle;
626 |
627 | if (!this._circle) {
628 | this._circle = circle(latlng, radius, style).addTo(this._layer);
629 | } else {
630 | this._circle.setLatLng(latlng).setRadius(radius).setStyle(style);
631 | }
632 | }
633 |
634 | let distance;
635 | let unit;
636 | if (this.options.metric) {
637 | distance = radius.toFixed(0);
638 | unit = this.options.strings.metersUnit;
639 | } else {
640 | distance = (radius * 3.2808399).toFixed(0);
641 | unit = this.options.strings.feetUnit;
642 | }
643 |
644 | // small inner marker
645 | if (this.options.drawMarker) {
646 | const mStyle = this._isFollowing() ? this.options.followMarkerStyle : this.options.markerStyle;
647 | if (!this._marker) {
648 | this._marker = new this.options.markerClass(latlng, mStyle).addTo(this._layer);
649 | } else {
650 | this._marker.setLatLng(latlng);
651 | // If the markerClass can be updated with setStyle, update it.
652 | if (this._marker.setStyle) {
653 | this._marker.setStyle(mStyle);
654 | }
655 | }
656 | }
657 |
658 | this._drawCompass();
659 |
660 | const t = this.options.strings.popup;
661 | function getPopupText() {
662 | if (typeof t === "string") {
663 | return LeafletUtil.template(t, { distance, unit });
664 | } else if (typeof t === "function") {
665 | return t({ distance, unit });
666 | } else {
667 | return t;
668 | }
669 | }
670 | if (this.options.showPopup && t && this._marker) {
671 | this._marker.bindPopup(getPopupText())._popup.setLatLng(latlng);
672 | }
673 | if (this.options.showPopup && t && this._compass) {
674 | this._compass.bindPopup(getPopupText())._popup.setLatLng(latlng);
675 | }
676 | },
677 |
678 | /**
679 | * Remove the marker from map.
680 | */
681 | _removeMarker() {
682 | this._layer.clearLayers();
683 | this._marker = undefined;
684 | this._circle = undefined;
685 | },
686 |
687 | /**
688 | * Unload the plugin and all event listeners.
689 | * Kind of the opposite of onAdd.
690 | */
691 | _unload() {
692 | this.stop();
693 | // May become undefined during HMR
694 | if (this._map) {
695 | this._map.off("unload", this._unload, this);
696 | }
697 | },
698 |
699 | /**
700 | * Sets the compass heading
701 | */
702 | _setCompassHeading(angle) {
703 | if (!isNaN(parseFloat(angle)) && isFinite(angle)) {
704 | angle = Math.round(angle);
705 |
706 | this._compassHeading = angle;
707 | LeafletUtil.requestAnimFrame(this._drawCompass, this);
708 | } else {
709 | this._compassHeading = null;
710 | }
711 | },
712 |
713 | /**
714 | * If the compass fails calibration just fail safely and remove the compass
715 | */
716 | _onCompassNeedsCalibration() {
717 | this._setCompassHeading();
718 | },
719 |
720 | /**
721 | * Process and normalise compass events
722 | */
723 | _onDeviceOrientation(e) {
724 | if (!this._active) {
725 | return;
726 | }
727 |
728 | if (e.webkitCompassHeading) {
729 | // iOS
730 | this._setCompassHeading(e.webkitCompassHeading);
731 | } else if (e.absolute && e.alpha) {
732 | // Android
733 | this._setCompassHeading(360 - e.alpha);
734 | }
735 | },
736 |
737 | /**
738 | * Calls deactivate and dispatches an error.
739 | */
740 | _onLocationError(err) {
741 | // ignore time out error if the location is watched
742 | if (err.code == 3 && this.options.locateOptions.watch) {
743 | return;
744 | }
745 |
746 | this.stop();
747 | this.options.onLocationError(err, this);
748 | },
749 |
750 | /**
751 | * Stores the received event and updates the marker.
752 | */
753 | _onLocationFound(e) {
754 | // no need to do anything if the location has not changed
755 | if (this._event && this._event.latlng.lat === e.latlng.lat && this._event.latlng.lng === e.latlng.lng && this._event.accuracy === e.accuracy) {
756 | return;
757 | }
758 |
759 | if (!this._active) {
760 | // we may have a stray event
761 | return;
762 | }
763 |
764 | this._event = e;
765 |
766 | this._drawMarker();
767 | this._updateContainerStyle();
768 |
769 | switch (this.options.setView) {
770 | case "once":
771 | if (this._justClicked) {
772 | this.setView();
773 | }
774 | break;
775 | case "untilPan":
776 | if (!this._userPanned) {
777 | this.setView();
778 | }
779 | break;
780 | case "untilPanOrZoom":
781 | if (!this._userPanned && !this._userZoomed) {
782 | this.setView();
783 | }
784 | break;
785 | case "always":
786 | this.setView();
787 | break;
788 | case false:
789 | // don't set the view
790 | break;
791 | }
792 |
793 | this._justClicked = false;
794 | },
795 |
796 | /**
797 | * When the user drags. Need a separate event so we can bind and unbind event listeners.
798 | */
799 | _onDrag() {
800 | // only react to drags once we have a location
801 | if (this._event && !this._ignoreEvent) {
802 | this._userPanned = true;
803 | this._updateContainerStyle();
804 | this._drawMarker();
805 | }
806 | },
807 |
808 | /**
809 | * When the user zooms. Need a separate event so we can bind and unbind event listeners.
810 | */
811 | _onZoom() {
812 | // only react to drags once we have a location
813 | if (this._event && !this._ignoreEvent) {
814 | this._userZoomed = true;
815 | this._updateContainerStyle();
816 | this._drawMarker();
817 | }
818 | },
819 |
820 | /**
821 | * After a zoom ends update the compass and handle sideways zooms
822 | */
823 | _onZoomEnd() {
824 | if (this._event) {
825 | this._drawCompass();
826 | }
827 |
828 | if (this._event && !this._ignoreEvent) {
829 | // If we have zoomed in and out and ended up sideways treat it as a pan
830 | if (this._marker && !this._map.getBounds().pad(-0.3).contains(this._marker.getLatLng())) {
831 | this._userPanned = true;
832 | this._updateContainerStyle();
833 | this._drawMarker();
834 | }
835 | }
836 | },
837 |
838 | /**
839 | * Compute whether the map is following the user location with pan and zoom.
840 | */
841 | _isFollowing() {
842 | if (!this._active) {
843 | return false;
844 | }
845 |
846 | if (this.options.setView === "always") {
847 | return true;
848 | } else if (this.options.setView === "untilPan") {
849 | return !this._userPanned;
850 | } else if (this.options.setView === "untilPanOrZoom") {
851 | return !this._userPanned && !this._userZoomed;
852 | }
853 | },
854 |
855 | /**
856 | * Check if location is in map bounds
857 | */
858 | _isOutsideMapBounds() {
859 | if (this._event === undefined) {
860 | return false;
861 | }
862 | return this._map.options.maxBounds && !this._map.options.maxBounds.contains(this._event.latlng);
863 | },
864 |
865 | /**
866 | * Toggles button class between following and active.
867 | */
868 | _updateContainerStyle() {
869 | if (!this._container) {
870 | return;
871 | }
872 |
873 | if (this._active && !this._event) {
874 | // active but don't have a location yet
875 | this._setClasses("requesting");
876 | } else if (this._isFollowing()) {
877 | this._setClasses("following");
878 | } else if (this._active) {
879 | this._setClasses("active");
880 | } else {
881 | this._cleanClasses();
882 | }
883 | },
884 |
885 | /**
886 | * Sets the CSS classes for the state.
887 | */
888 | _setClasses(state) {
889 | if (state == "requesting") {
890 | removeClasses(this._container, "active following");
891 | addClasses(this._container, "requesting");
892 |
893 | removeClasses(this._icon, this.options.icon);
894 | addClasses(this._icon, this.options.iconLoading);
895 | } else if (state == "active") {
896 | removeClasses(this._container, "requesting following");
897 | addClasses(this._container, "active");
898 |
899 | removeClasses(this._icon, this.options.iconLoading);
900 | addClasses(this._icon, this.options.icon);
901 | } else if (state == "following") {
902 | removeClasses(this._container, "requesting");
903 | addClasses(this._container, "active following");
904 |
905 | removeClasses(this._icon, this.options.iconLoading);
906 | addClasses(this._icon, this.options.icon);
907 | }
908 | },
909 |
910 | /**
911 | * Removes all classes from button.
912 | */
913 | _cleanClasses() {
914 | DomUtil.removeClass(this._container, "requesting");
915 | DomUtil.removeClass(this._container, "active");
916 | DomUtil.removeClass(this._container, "following");
917 |
918 | removeClasses(this._icon, this.options.iconLoading);
919 | addClasses(this._icon, this.options.icon);
920 | },
921 |
922 | /**
923 | * Reinitializes state variables.
924 | */
925 | _resetVariables() {
926 | // whether locate is active or not
927 | this._active = false;
928 |
929 | // true if the control was clicked for the first time
930 | // we need this so we can pan and zoom once we have the location
931 | this._justClicked = false;
932 |
933 | // true if the user has panned the map after clicking the control
934 | this._userPanned = false;
935 |
936 | // true if the user has zoomed the map after clicking the control
937 | this._userZoomed = false;
938 | }
939 | });
940 |
941 | function locate(options) {
942 | return new LocateControl(options);
943 | }
944 |
945 | export { LocationMarker, CompassMarker, LocateControl, locate };
946 |
--------------------------------------------------------------------------------
/dist/L.Control.Locate.umd.js:
--------------------------------------------------------------------------------
1 | (function (global, factory) {
2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('leaflet')) :
3 | typeof define === 'function' && define.amd ? define(['exports', 'leaflet'], factory) :
4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.L = global.L || {}, global.L.Control = global.L.Control || {}, global.L.Control.Locate = {}), global.L));
5 | })(this, (function (exports, leaflet) { 'use strict';
6 |
7 | /*!
8 | Copyright (c) 2016 Dominik Moritz
9 |
10 | This file is part of the leaflet locate control. It is licensed under the MIT license.
11 | You can find the project at: https://github.com/domoritz/leaflet-locatecontrol
12 | */
13 |
14 | const addClasses = (el, names) => {
15 | names.split(" ").forEach((className) => {
16 | el.classList.add(className);
17 | });
18 | };
19 |
20 | const removeClasses = (el, names) => {
21 | names.split(" ").forEach((className) => {
22 | el.classList.remove(className);
23 | });
24 | };
25 |
26 | /**
27 | * Compatible with Circle but a true marker instead of a path
28 | */
29 | const LocationMarker = leaflet.Marker.extend({
30 | initialize(latlng, options) {
31 | leaflet.setOptions(this, options);
32 | this._latlng = latlng;
33 | this.createIcon();
34 | },
35 |
36 | /**
37 | * Create a styled circle location marker
38 | */
39 | createIcon() {
40 | const opt = this.options;
41 |
42 | const style = [
43 | ["stroke", opt.color],
44 | ["stroke-width", opt.weight],
45 | ["fill", opt.fillColor],
46 | ["fill-opacity", opt.fillOpacity],
47 | ["opacity", opt.opacity]
48 | ]
49 | .filter(([k,v]) => v !== undefined)
50 | .map(([k,v]) => `${k}="${v}"`)
51 | .join(" ");
52 |
53 | const icon = this._getIconSVG(opt, style);
54 |
55 | this._locationIcon = leaflet.divIcon({
56 | className: icon.className,
57 | html: icon.svg,
58 | iconSize: [icon.w, icon.h]
59 | });
60 |
61 | this.setIcon(this._locationIcon);
62 | },
63 |
64 | /**
65 | * Return the raw svg for the shape
66 | *
67 | * Split so can be easily overridden
68 | */
69 | _getIconSVG(options, style) {
70 | const r = options.radius;
71 | const w = options.weight;
72 | const s = r + w;
73 | const s2 = s * 2;
74 | const svg =
75 | `` +
76 | ` `;
77 | return {
78 | className: "leaflet-control-locate-location",
79 | svg,
80 | w: s2,
81 | h: s2
82 | };
83 | },
84 |
85 | setStyle(style) {
86 | leaflet.setOptions(this, style);
87 | this.createIcon();
88 | }
89 | });
90 |
91 | const CompassMarker = LocationMarker.extend({
92 | initialize(latlng, heading, options) {
93 | leaflet.setOptions(this, options);
94 | this._latlng = latlng;
95 | this._heading = heading;
96 | this.createIcon();
97 | },
98 |
99 | setHeading(heading) {
100 | this._heading = heading;
101 | },
102 |
103 | /**
104 | * Create a styled arrow compass marker
105 | */
106 | _getIconSVG(options, style) {
107 | const r = options.radius;
108 | const s = r + options.weight + options.depth;
109 | const s2 = s * 2;
110 |
111 | const path = this._arrowPoints(r, options.width, options.depth, this._heading);
112 |
113 | const svg =
114 | `` +
115 | ` `;
116 | return {
117 | className: "leaflet-control-locate-heading",
118 | svg,
119 | w: s2,
120 | h: s2
121 | };
122 | },
123 |
124 | _arrowPoints(radius, width, depth, heading) {
125 | const φ = ((heading - 90) * Math.PI) / 180;
126 | const ux = Math.cos(φ);
127 | const uy = Math.sin(φ);
128 | const vx = -Math.sin(φ);
129 | const vy = Math.cos(φ);
130 | const h = width / 2;
131 |
132 | // Base center on circle
133 | const Cx = radius * ux;
134 | const Cy = radius * uy;
135 |
136 | // Base corners
137 | const B1x = Cx + h * vx;
138 | const B1y = Cy + h * vy;
139 | const B2x = Cx - h * vx;
140 | const B2y = Cy - h * vy;
141 |
142 | // Tip outward
143 | const Tx = Cx + depth * ux;
144 | const Ty = Cy + depth * uy;
145 |
146 | return `M ${B1x},${B1y} L ${B2x},${B2y} L ${Tx},${Ty} Z`;
147 | }
148 | });
149 |
150 | const LocateControl = leaflet.Control.extend({
151 | options: {
152 | /** Position of the control */
153 | position: "topleft",
154 | /** The layer that the user's location should be drawn on. By default creates a new layer. */
155 | layer: undefined,
156 | /**
157 | * Automatically sets the map view (zoom and pan) to the user's location as it updates.
158 | * While the map is following the user's location, the control is in the `following` state,
159 | * which changes the style of the control and the circle marker.
160 | *
161 | * Possible values:
162 | * - false: never updates the map view when location changes.
163 | * - 'once': set the view when the location is first determined
164 | * - 'always': always updates the map view when location changes.
165 | * The map view follows the user's location.
166 | * - 'untilPan': like 'always', except stops updating the
167 | * view if the user has manually panned the map.
168 | * The map view follows the user's location until she pans.
169 | * - 'untilPanOrZoom': (default) like 'always', except stops updating the
170 | * view if the user has manually panned the map.
171 | * The map view follows the user's location until she pans.
172 | */
173 | setView: "untilPanOrZoom",
174 | /** Keep the current map zoom level when setting the view and only pan. */
175 | keepCurrentZoomLevel: false,
176 | /** 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. */
177 | initialZoomLevel: false,
178 | /**
179 | * This callback can be used to override the viewport tracking
180 | * This function should return a LatLngBounds object.
181 | *
182 | * For example to extend the viewport to ensure that a particular LatLng is visible:
183 | *
184 | * getLocationBounds: function(locationEvent) {
185 | * return locationEvent.bounds.extend([-33.873085, 151.219273]);
186 | * },
187 | */
188 | getLocationBounds(locationEvent) {
189 | return locationEvent.bounds;
190 | },
191 | /** Smooth pan and zoom to the location of the marker. Only works in Leaflet 1.0+. */
192 | flyTo: false,
193 | /**
194 | * The user location can be inside and outside the current view when the user clicks on the
195 | * control that is already active. Both cases can be configures separately.
196 | * Possible values are:
197 | * - 'setView': zoom and pan to the current location
198 | * - 'stop': stop locating and remove the location marker
199 | */
200 | clickBehavior: {
201 | /** What should happen if the user clicks on the control while the location is within the current view. */
202 | inView: "stop",
203 | /** What should happen if the user clicks on the control while the location is outside the current view. */
204 | outOfView: "setView",
205 | /**
206 | * What should happen if the user clicks on the control while the location is within the current view
207 | * and we could be following but are not. Defaults to a special value which inherits from 'inView';
208 | */
209 | inViewNotFollowing: "inView"
210 | },
211 | /**
212 | * If set, save the map bounds just before centering to the user's
213 | * location. When control is disabled, set the view back to the
214 | * bounds that were saved.
215 | */
216 | returnToPrevBounds: false,
217 | /**
218 | * Keep a cache of the location after the user deactivates the control. If set to false, the user has to wait
219 | * until the locate API returns a new location before they see where they are again.
220 | */
221 | cacheLocation: true,
222 | /** If set, a circle that shows the location accuracy is drawn. */
223 | drawCircle: true,
224 | /** If set, the marker at the users' location is drawn. */
225 | drawMarker: true,
226 | /** If set and supported then show the compass heading */
227 | showCompass: true,
228 | /** The class to be used to create the marker. For example L.CircleMarker or L.Marker */
229 | markerClass: LocationMarker,
230 | /** The class us be used to create the compass bearing arrow */
231 | compassClass: CompassMarker,
232 | /** Accuracy circle style properties. NOTE these styles should match the css animations styles */
233 | circleStyle: {
234 | className: "leaflet-control-locate-circle",
235 | color: "#136AEC",
236 | fillColor: "#136AEC",
237 | fillOpacity: 0.15,
238 | weight: 0
239 | },
240 | /** Inner marker style properties. Only works if your marker class supports `setStyle`. */
241 | markerStyle: {
242 | className: "leaflet-control-locate-marker",
243 | color: "#fff",
244 | fillColor: "#2A93EE",
245 | fillOpacity: 1,
246 | weight: 3,
247 | opacity: 1,
248 | radius: 9
249 | },
250 | /** Compass */
251 | compassStyle: {
252 | fillColor: "#2A93EE",
253 | fillOpacity: 1,
254 | weight: 0,
255 | color: "#fff",
256 | opacity: 1,
257 | radius: 9, // How far is the arrow from the center of the marker
258 | width: 9, // Width of the arrow
259 | depth: 6 // Length of the arrow
260 | },
261 | /**
262 | * Changes to accuracy circle and inner marker while following.
263 | * It is only necessary to provide the properties that should change.
264 | */
265 | followCircleStyle: {},
266 | followMarkerStyle: {
267 | // color: '#FFA500',
268 | // fillColor: '#FFB000'
269 | },
270 | followCompassStyle: {},
271 | /** The CSS class for the icon. For example fa-location-arrow or fa-map-marker */
272 | icon: "leaflet-control-locate-location-arrow",
273 | iconLoading: "leaflet-control-locate-spinner",
274 | /** The element to be created for icons. For example span or i */
275 | iconElementTag: "span",
276 | /** The element to be created for the text. For example small or span */
277 | textElementTag: "small",
278 | /** Padding around the accuracy circle. */
279 | circlePadding: [0, 0],
280 | /** Use metric units. */
281 | metric: true,
282 | /**
283 | * This callback can be used in case you would like to override button creation behavior.
284 | * This is useful for DOM manipulation frameworks such as angular etc.
285 | * This function should return an object with HtmlElement for the button (link property) and the icon (icon property).
286 | */
287 | createButtonCallback(container, options) {
288 | const link = leaflet.DomUtil.create("a", "leaflet-bar-part leaflet-bar-part-single", container);
289 | link.title = options.strings.title;
290 | link.href = "#";
291 | link.setAttribute("role", "button");
292 | const icon = leaflet.DomUtil.create(options.iconElementTag, options.icon, link);
293 |
294 | if (options.strings.text !== undefined) {
295 | const text = leaflet.DomUtil.create(options.textElementTag, "leaflet-locate-text", link);
296 | text.textContent = options.strings.text;
297 | link.classList.add("leaflet-locate-text-active");
298 | link.parentNode.style.display = "flex";
299 | if (options.icon.length > 0) {
300 | icon.classList.add("leaflet-locate-icon");
301 | }
302 | }
303 |
304 | return { link, icon };
305 | },
306 | /** This event is called in case of any location error that is not a time out error. */
307 | onLocationError(err, control) {
308 | alert(err.message);
309 | },
310 | /**
311 | * This event is called when the user's location is outside the bounds set on the map.
312 | * The event is called repeatedly when the location changes.
313 | */
314 | onLocationOutsideMapBounds(control) {
315 | control.stop();
316 | alert(control.options.strings.outsideMapBoundsMsg);
317 | },
318 | /** Display a pop-up when the user click on the inner marker. */
319 | showPopup: true,
320 | strings: {
321 | title: "Show me where I am",
322 | metersUnit: "meters",
323 | feetUnit: "feet",
324 | popup: "You are within {distance} {unit} from this point",
325 | outsideMapBoundsMsg: "You seem located outside the boundaries of the map"
326 | },
327 | /** The default options passed to leaflets locate method. */
328 | locateOptions: {
329 | maxZoom: Infinity,
330 | watch: true, // if you overwrite this, visualization cannot be updated
331 | setView: false // have to set this to false because we have to
332 | // do setView manually
333 | }
334 | },
335 |
336 | initialize(options) {
337 | // set default options if nothing is set (merge one step deep)
338 | for (const i in options) {
339 | if (typeof this.options[i] === "object") {
340 | leaflet.extend(this.options[i], options[i]);
341 | } else {
342 | this.options[i] = options[i];
343 | }
344 | }
345 |
346 | // extend the follow marker style and circle from the normal style
347 | this.options.followMarkerStyle = leaflet.extend({}, this.options.markerStyle, this.options.followMarkerStyle);
348 | this.options.followCircleStyle = leaflet.extend({}, this.options.circleStyle, this.options.followCircleStyle);
349 | this.options.followCompassStyle = leaflet.extend({}, this.options.compassStyle, this.options.followCompassStyle);
350 | },
351 |
352 | /**
353 | * Add control to map. Returns the container for the control.
354 | */
355 | onAdd(map) {
356 | const container = leaflet.DomUtil.create("div", "leaflet-control-locate leaflet-bar leaflet-control");
357 | this._container = container;
358 | this._map = map;
359 | this._layer = this.options.layer || new leaflet.LayerGroup();
360 | this._layer.addTo(map);
361 | this._event = undefined;
362 | this._compassHeading = null;
363 | this._prevBounds = null;
364 |
365 | const linkAndIcon = this.options.createButtonCallback(container, this.options);
366 | this._link = linkAndIcon.link;
367 | this._icon = linkAndIcon.icon;
368 |
369 | leaflet.DomEvent.on(
370 | this._link,
371 | "click",
372 | function (ev) {
373 | leaflet.DomEvent.stopPropagation(ev);
374 | leaflet.DomEvent.preventDefault(ev);
375 | this._onClick();
376 | },
377 | this
378 | ).on(this._link, "dblclick", leaflet.DomEvent.stopPropagation);
379 |
380 | this._resetVariables();
381 |
382 | this._map.on("unload", this._unload, this);
383 |
384 | return container;
385 | },
386 |
387 | /**
388 | * This method is called when the user clicks on the control.
389 | */
390 | _onClick() {
391 | this._justClicked = true;
392 | const wasFollowing = this._isFollowing();
393 | this._userPanned = false;
394 | this._userZoomed = false;
395 |
396 | if (this._active && !this._event) {
397 | // click while requesting
398 | this.stop();
399 | } else if (this._active) {
400 | const behaviors = this.options.clickBehavior;
401 | let behavior = behaviors.outOfView;
402 | if (this._map.getBounds().contains(this._event.latlng)) {
403 | behavior = wasFollowing ? behaviors.inView : behaviors.inViewNotFollowing;
404 | }
405 |
406 | // Allow inheriting from another behavior
407 | if (behaviors[behavior]) {
408 | behavior = behaviors[behavior];
409 | }
410 |
411 | switch (behavior) {
412 | case "setView":
413 | this.setView();
414 | break;
415 | case "stop":
416 | this.stop();
417 | if (this.options.returnToPrevBounds) {
418 | const f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds;
419 | f.bind(this._map)(this._prevBounds);
420 | }
421 | break;
422 | }
423 | } else {
424 | if (this.options.returnToPrevBounds) {
425 | this._prevBounds = this._map.getBounds();
426 | }
427 | this.start();
428 | }
429 |
430 | this._updateContainerStyle();
431 | },
432 |
433 | /**
434 | * Starts the plugin:
435 | * - activates the engine
436 | * - draws the marker (if coordinates available)
437 | */
438 | start() {
439 | this._activate();
440 |
441 | if (this._event) {
442 | this._drawMarker(this._map);
443 |
444 | // if we already have a location but the user clicked on the control
445 | if (this.options.setView) {
446 | this.setView();
447 | }
448 | }
449 | this._updateContainerStyle();
450 | },
451 |
452 | /**
453 | * Stops the plugin:
454 | * - deactivates the engine
455 | * - reinitializes the button
456 | * - removes the marker
457 | */
458 | stop() {
459 | this._deactivate();
460 |
461 | this._cleanClasses();
462 | this._resetVariables();
463 |
464 | this._removeMarker();
465 | },
466 |
467 | /**
468 | * Keep the control active but stop following the location
469 | */
470 | stopFollowing() {
471 | this._userPanned = true;
472 | this._updateContainerStyle();
473 | this._drawMarker();
474 | },
475 |
476 | /**
477 | * This method launches the location engine.
478 | * It is called before the marker is updated,
479 | * event if it does not mean that the event will be ready.
480 | *
481 | * Override it if you want to add more functionalities.
482 | * It should set the this._active to true and do nothing if
483 | * this._active is true.
484 | */
485 | _activate() {
486 | if (this._active || !this._map) {
487 | return;
488 | }
489 |
490 | this._map.locate(this.options.locateOptions);
491 | this._map.fire("locateactivate", this);
492 | this._active = true;
493 |
494 | // bind event listeners
495 | this._map.on("locationfound", this._onLocationFound, this);
496 | this._map.on("locationerror", this._onLocationError, this);
497 | this._map.on("dragstart", this._onDrag, this);
498 | this._map.on("zoomstart", this._onZoom, this);
499 | this._map.on("zoomend", this._onZoomEnd, this);
500 | if (this.options.showCompass) {
501 | const oriAbs = "ondeviceorientationabsolute" in window;
502 | if (oriAbs || "ondeviceorientation" in window) {
503 | const _this = this;
504 | const deviceorientation = function () {
505 | leaflet.DomEvent.on(window, oriAbs ? "deviceorientationabsolute" : "deviceorientation", _this._onDeviceOrientation, _this);
506 | };
507 | if (DeviceOrientationEvent && typeof DeviceOrientationEvent.requestPermission === "function") {
508 | DeviceOrientationEvent.requestPermission().then(function (permissionState) {
509 | if (permissionState === "granted") {
510 | deviceorientation();
511 | }
512 | });
513 | } else {
514 | deviceorientation();
515 | }
516 | }
517 | }
518 | },
519 |
520 | /**
521 | * Called to stop the location engine.
522 | *
523 | * Override it to shutdown any functionalities you added on start.
524 | */
525 | _deactivate() {
526 | if (!this._active || !this._map) {
527 | return;
528 | }
529 |
530 | this._map.stopLocate();
531 | this._map.fire("locatedeactivate", this);
532 | this._active = false;
533 |
534 | if (!this.options.cacheLocation) {
535 | this._event = undefined;
536 | }
537 |
538 | // unbind event listeners
539 | this._map.off("locationfound", this._onLocationFound, this);
540 | this._map.off("locationerror", this._onLocationError, this);
541 | this._map.off("dragstart", this._onDrag, this);
542 | this._map.off("zoomstart", this._onZoom, this);
543 | this._map.off("zoomend", this._onZoomEnd, this);
544 | if (this.options.showCompass) {
545 | this._compassHeading = null;
546 | if ("ondeviceorientationabsolute" in window) {
547 | leaflet.DomEvent.off(window, "deviceorientationabsolute", this._onDeviceOrientation, this);
548 | } else if ("ondeviceorientation" in window) {
549 | leaflet.DomEvent.off(window, "deviceorientation", this._onDeviceOrientation, this);
550 | }
551 | }
552 | },
553 |
554 | /**
555 | * Zoom (unless we should keep the zoom level) and an to the current view.
556 | */
557 | setView() {
558 | this._drawMarker();
559 | if (this._isOutsideMapBounds()) {
560 | this._event = undefined; // clear the current location so we can get back into the bounds
561 | this.options.onLocationOutsideMapBounds(this);
562 | } else {
563 | if (this._justClicked && this.options.initialZoomLevel !== false) {
564 | let f = this.options.flyTo ? this._map.flyTo : this._map.setView;
565 | f.bind(this._map)([this._event.latitude, this._event.longitude], this.options.initialZoomLevel);
566 | } else if (this.options.keepCurrentZoomLevel) {
567 | let f = this.options.flyTo ? this._map.flyTo : this._map.panTo;
568 | f.bind(this._map)([this._event.latitude, this._event.longitude]);
569 | } else {
570 | let f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds;
571 | // Ignore zoom events while setting the viewport as these would stop following
572 | this._ignoreEvent = true;
573 | f.bind(this._map)(this.options.getLocationBounds(this._event), {
574 | padding: this.options.circlePadding,
575 | maxZoom: this.options.initialZoomLevel || this.options.locateOptions.maxZoom
576 | });
577 | leaflet.Util.requestAnimFrame(function () {
578 | // Wait until after the next animFrame because the flyTo can be async
579 | this._ignoreEvent = false;
580 | }, this);
581 | }
582 | }
583 | },
584 |
585 | /**
586 | *
587 | */
588 | _drawCompass() {
589 | if (!this._event) {
590 | return;
591 | }
592 |
593 | const latlng = this._event.latlng;
594 |
595 | if (this.options.showCompass && latlng && this._compassHeading !== null) {
596 | const cStyle = this._isFollowing() ? this.options.followCompassStyle : this.options.compassStyle;
597 | if (!this._compass) {
598 | this._compass = new this.options.compassClass(latlng, this._compassHeading, cStyle).addTo(this._layer);
599 | } else {
600 | this._compass.setLatLng(latlng);
601 | this._compass.setHeading(this._compassHeading);
602 | // If the compassClass can be updated with setStyle, update it.
603 | if (this._compass.setStyle) {
604 | this._compass.setStyle(cStyle);
605 | }
606 | }
607 | //
608 | }
609 | if (this._compass && (!this.options.showCompass || this._compassHeading === null)) {
610 | this._compass.removeFrom(this._layer);
611 | this._compass = null;
612 | }
613 | },
614 |
615 | /**
616 | * Draw the marker and accuracy circle on the map.
617 | *
618 | * Uses the event retrieved from onLocationFound from the map.
619 | */
620 | _drawMarker() {
621 | if (this._event.accuracy === undefined) {
622 | this._event.accuracy = 0;
623 | }
624 |
625 | const radius = this._event.accuracy;
626 | const latlng = this._event.latlng;
627 |
628 | // circle with the radius of the location's accuracy
629 | if (this.options.drawCircle) {
630 | const style = this._isFollowing() ? this.options.followCircleStyle : this.options.circleStyle;
631 |
632 | if (!this._circle) {
633 | this._circle = leaflet.circle(latlng, radius, style).addTo(this._layer);
634 | } else {
635 | this._circle.setLatLng(latlng).setRadius(radius).setStyle(style);
636 | }
637 | }
638 |
639 | let distance;
640 | let unit;
641 | if (this.options.metric) {
642 | distance = radius.toFixed(0);
643 | unit = this.options.strings.metersUnit;
644 | } else {
645 | distance = (radius * 3.2808399).toFixed(0);
646 | unit = this.options.strings.feetUnit;
647 | }
648 |
649 | // small inner marker
650 | if (this.options.drawMarker) {
651 | const mStyle = this._isFollowing() ? this.options.followMarkerStyle : this.options.markerStyle;
652 | if (!this._marker) {
653 | this._marker = new this.options.markerClass(latlng, mStyle).addTo(this._layer);
654 | } else {
655 | this._marker.setLatLng(latlng);
656 | // If the markerClass can be updated with setStyle, update it.
657 | if (this._marker.setStyle) {
658 | this._marker.setStyle(mStyle);
659 | }
660 | }
661 | }
662 |
663 | this._drawCompass();
664 |
665 | const t = this.options.strings.popup;
666 | function getPopupText() {
667 | if (typeof t === "string") {
668 | return leaflet.Util.template(t, { distance, unit });
669 | } else if (typeof t === "function") {
670 | return t({ distance, unit });
671 | } else {
672 | return t;
673 | }
674 | }
675 | if (this.options.showPopup && t && this._marker) {
676 | this._marker.bindPopup(getPopupText())._popup.setLatLng(latlng);
677 | }
678 | if (this.options.showPopup && t && this._compass) {
679 | this._compass.bindPopup(getPopupText())._popup.setLatLng(latlng);
680 | }
681 | },
682 |
683 | /**
684 | * Remove the marker from map.
685 | */
686 | _removeMarker() {
687 | this._layer.clearLayers();
688 | this._marker = undefined;
689 | this._circle = undefined;
690 | },
691 |
692 | /**
693 | * Unload the plugin and all event listeners.
694 | * Kind of the opposite of onAdd.
695 | */
696 | _unload() {
697 | this.stop();
698 | // May become undefined during HMR
699 | if (this._map) {
700 | this._map.off("unload", this._unload, this);
701 | }
702 | },
703 |
704 | /**
705 | * Sets the compass heading
706 | */
707 | _setCompassHeading(angle) {
708 | if (!isNaN(parseFloat(angle)) && isFinite(angle)) {
709 | angle = Math.round(angle);
710 |
711 | this._compassHeading = angle;
712 | leaflet.Util.requestAnimFrame(this._drawCompass, this);
713 | } else {
714 | this._compassHeading = null;
715 | }
716 | },
717 |
718 | /**
719 | * If the compass fails calibration just fail safely and remove the compass
720 | */
721 | _onCompassNeedsCalibration() {
722 | this._setCompassHeading();
723 | },
724 |
725 | /**
726 | * Process and normalise compass events
727 | */
728 | _onDeviceOrientation(e) {
729 | if (!this._active) {
730 | return;
731 | }
732 |
733 | if (e.webkitCompassHeading) {
734 | // iOS
735 | this._setCompassHeading(e.webkitCompassHeading);
736 | } else if (e.absolute && e.alpha) {
737 | // Android
738 | this._setCompassHeading(360 - e.alpha);
739 | }
740 | },
741 |
742 | /**
743 | * Calls deactivate and dispatches an error.
744 | */
745 | _onLocationError(err) {
746 | // ignore time out error if the location is watched
747 | if (err.code == 3 && this.options.locateOptions.watch) {
748 | return;
749 | }
750 |
751 | this.stop();
752 | this.options.onLocationError(err, this);
753 | },
754 |
755 | /**
756 | * Stores the received event and updates the marker.
757 | */
758 | _onLocationFound(e) {
759 | // no need to do anything if the location has not changed
760 | if (this._event && this._event.latlng.lat === e.latlng.lat && this._event.latlng.lng === e.latlng.lng && this._event.accuracy === e.accuracy) {
761 | return;
762 | }
763 |
764 | if (!this._active) {
765 | // we may have a stray event
766 | return;
767 | }
768 |
769 | this._event = e;
770 |
771 | this._drawMarker();
772 | this._updateContainerStyle();
773 |
774 | switch (this.options.setView) {
775 | case "once":
776 | if (this._justClicked) {
777 | this.setView();
778 | }
779 | break;
780 | case "untilPan":
781 | if (!this._userPanned) {
782 | this.setView();
783 | }
784 | break;
785 | case "untilPanOrZoom":
786 | if (!this._userPanned && !this._userZoomed) {
787 | this.setView();
788 | }
789 | break;
790 | case "always":
791 | this.setView();
792 | break;
793 | }
794 |
795 | this._justClicked = false;
796 | },
797 |
798 | /**
799 | * When the user drags. Need a separate event so we can bind and unbind event listeners.
800 | */
801 | _onDrag() {
802 | // only react to drags once we have a location
803 | if (this._event && !this._ignoreEvent) {
804 | this._userPanned = true;
805 | this._updateContainerStyle();
806 | this._drawMarker();
807 | }
808 | },
809 |
810 | /**
811 | * When the user zooms. Need a separate event so we can bind and unbind event listeners.
812 | */
813 | _onZoom() {
814 | // only react to drags once we have a location
815 | if (this._event && !this._ignoreEvent) {
816 | this._userZoomed = true;
817 | this._updateContainerStyle();
818 | this._drawMarker();
819 | }
820 | },
821 |
822 | /**
823 | * After a zoom ends update the compass and handle sideways zooms
824 | */
825 | _onZoomEnd() {
826 | if (this._event) {
827 | this._drawCompass();
828 | }
829 |
830 | if (this._event && !this._ignoreEvent) {
831 | // If we have zoomed in and out and ended up sideways treat it as a pan
832 | if (this._marker && !this._map.getBounds().pad(-0.3).contains(this._marker.getLatLng())) {
833 | this._userPanned = true;
834 | this._updateContainerStyle();
835 | this._drawMarker();
836 | }
837 | }
838 | },
839 |
840 | /**
841 | * Compute whether the map is following the user location with pan and zoom.
842 | */
843 | _isFollowing() {
844 | if (!this._active) {
845 | return false;
846 | }
847 |
848 | if (this.options.setView === "always") {
849 | return true;
850 | } else if (this.options.setView === "untilPan") {
851 | return !this._userPanned;
852 | } else if (this.options.setView === "untilPanOrZoom") {
853 | return !this._userPanned && !this._userZoomed;
854 | }
855 | },
856 |
857 | /**
858 | * Check if location is in map bounds
859 | */
860 | _isOutsideMapBounds() {
861 | if (this._event === undefined) {
862 | return false;
863 | }
864 | return this._map.options.maxBounds && !this._map.options.maxBounds.contains(this._event.latlng);
865 | },
866 |
867 | /**
868 | * Toggles button class between following and active.
869 | */
870 | _updateContainerStyle() {
871 | if (!this._container) {
872 | return;
873 | }
874 |
875 | if (this._active && !this._event) {
876 | // active but don't have a location yet
877 | this._setClasses("requesting");
878 | } else if (this._isFollowing()) {
879 | this._setClasses("following");
880 | } else if (this._active) {
881 | this._setClasses("active");
882 | } else {
883 | this._cleanClasses();
884 | }
885 | },
886 |
887 | /**
888 | * Sets the CSS classes for the state.
889 | */
890 | _setClasses(state) {
891 | if (state == "requesting") {
892 | removeClasses(this._container, "active following");
893 | addClasses(this._container, "requesting");
894 |
895 | removeClasses(this._icon, this.options.icon);
896 | addClasses(this._icon, this.options.iconLoading);
897 | } else if (state == "active") {
898 | removeClasses(this._container, "requesting following");
899 | addClasses(this._container, "active");
900 |
901 | removeClasses(this._icon, this.options.iconLoading);
902 | addClasses(this._icon, this.options.icon);
903 | } else if (state == "following") {
904 | removeClasses(this._container, "requesting");
905 | addClasses(this._container, "active following");
906 |
907 | removeClasses(this._icon, this.options.iconLoading);
908 | addClasses(this._icon, this.options.icon);
909 | }
910 | },
911 |
912 | /**
913 | * Removes all classes from button.
914 | */
915 | _cleanClasses() {
916 | leaflet.DomUtil.removeClass(this._container, "requesting");
917 | leaflet.DomUtil.removeClass(this._container, "active");
918 | leaflet.DomUtil.removeClass(this._container, "following");
919 |
920 | removeClasses(this._icon, this.options.iconLoading);
921 | addClasses(this._icon, this.options.icon);
922 | },
923 |
924 | /**
925 | * Reinitializes state variables.
926 | */
927 | _resetVariables() {
928 | // whether locate is active or not
929 | this._active = false;
930 |
931 | // true if the control was clicked for the first time
932 | // we need this so we can pan and zoom once we have the location
933 | this._justClicked = false;
934 |
935 | // true if the user has panned the map after clicking the control
936 | this._userPanned = false;
937 |
938 | // true if the user has zoomed the map after clicking the control
939 | this._userZoomed = false;
940 | }
941 | });
942 |
943 | function locate(options) {
944 | return new LocateControl(options);
945 | }
946 |
947 | exports.CompassMarker = CompassMarker;
948 | exports.LocateControl = LocateControl;
949 | exports.LocationMarker = LocationMarker;
950 | exports.locate = locate;
951 |
952 | Object.defineProperty(exports, '__esModule', { value: true });
953 |
954 | }));
955 |
956 | (function() {
957 | if (typeof window !== 'undefined' && window.L) {
958 | window.L.control = window.L.control || {};
959 | window.L.control.locate = window.L.Control.Locate.locate;
960 | }
961 | })();
962 |
--------------------------------------------------------------------------------