├── .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 |
Fork me on GitHub
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 |
Fork me on GitHub
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 |
Fork me on GitHub
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 |
203 |
204 |
205 |
206 |

leaflet-locatecontrol

207 |
208 |

A useful control to geolocate the user with many options. Used by osm.org and mapbox among many others.

209 |

210 | MIT Licensed – View on GitHub or read the 211 | Leaflet documentation. 212 |

213 |
214 |
215 |
216 | leaflet-locatecontrol example map screenshot 217 |
218 |
219 |
220 |
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 | [![npm version](https://badge.fury.io/js/leaflet.locatecontrol.svg)](http://badge.fury.io/js/leaflet.locatecontrol) 4 | [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/leaflet.locatecontrol/badge?style=rounded)](https://www.jsdelivr.com/package/npm/leaflet.locatecontrol) 5 | 6 | A useful control to geolocate the user with many options. Official [Leaflet](http://leafletjs.com/plugins.html#geolocation) and [MapBox plugin](https://www.mapbox.com/mapbox.js/example/v1.0.0/leaflet-locatecontrol/). 7 | 8 | Tested with [Leaflet](http://leafletjs.com/) 1.9.2 and [Mapbox.js](https://docs.mapbox.com/mapbox.js/) 3.3.1 in Firefox, Chrome and Safari. 9 | 10 | Please check for [breaking changes in the changelog](https://github.com/domoritz/leaflet-locatecontrol/blob/gh-pages/CHANGELOG.md). 11 | 12 | ## Demo 13 | 14 | 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 | ![screenshot](https://raw.github.com/domoritz/leaflet-locatecontrol/gh-pages/screenshot.png "Screenshot showing the locate control") 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 | --------------------------------------------------------------------------------