├── .eslintrc.json ├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── package.json ├── packages ├── compass │ ├── README.md │ ├── package.json │ ├── src │ │ ├── icons.js │ │ ├── index.css │ │ ├── index.js │ │ └── types.js │ ├── tsconfig.json │ └── types │ │ ├── icons.d.ts │ │ ├── index.d.ts │ │ └── types.d.ts ├── helpers │ ├── package.json │ ├── src │ │ └── index.js │ ├── tsconfig.json │ └── types │ │ └── index.d.ts ├── image │ ├── README.md │ ├── package.json │ ├── src │ │ ├── center-position.js │ │ ├── file.js │ │ ├── icons.js │ │ ├── index.css │ │ ├── index.js │ │ ├── modes │ │ │ ├── move.js │ │ │ ├── rotate.js │ │ │ └── scale.js │ │ ├── raster.js │ │ └── types.js │ ├── tsconfig.json │ └── types │ │ ├── center-position.d.ts │ │ ├── file.d.ts │ │ ├── icons.d.ts │ │ ├── index.d.ts │ │ ├── modes │ │ ├── move.d.ts │ │ ├── rotate.d.ts │ │ └── scale.d.ts │ │ ├── raster.d.ts │ │ └── types.d.ts ├── inspect │ ├── README.md │ ├── package.json │ ├── src │ │ ├── icons.js │ │ ├── index.css │ │ ├── index.js │ │ ├── popup.js │ │ └── types.js │ ├── tsconfig.json │ └── types │ │ ├── icons.d.ts │ │ ├── index.d.ts │ │ ├── popup.d.ts │ │ └── types.d.ts ├── language │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.js │ │ └── types.js │ ├── tsconfig.json │ └── types │ │ ├── index.d.ts │ │ └── types.d.ts ├── ruler │ ├── README.md │ ├── package.json │ ├── src │ │ ├── icons.js │ │ ├── index.css │ │ ├── index.js │ │ ├── layers.js │ │ ├── sources.js │ │ └── types.js │ ├── tsconfig.json │ └── types │ │ ├── distance.d.ts │ │ ├── icons.d.ts │ │ ├── index.d.ts │ │ ├── label-format.d.ts │ │ ├── layers.d.ts │ │ ├── sources.d.ts │ │ └── types.d.ts ├── styles │ ├── README.md │ ├── package.json │ ├── src │ │ ├── icons.js │ │ ├── index.css │ │ ├── index.js │ │ └── types.js │ ├── tsconfig.json │ └── types │ │ ├── icons.d.ts │ │ ├── index.d.ts │ │ └── types.d.ts ├── tooltip │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.css │ │ ├── index.js │ │ └── types.js │ ├── tsconfig.json │ └── types │ │ ├── index.d.ts │ │ └── types.d.ts └── zoom │ ├── README.md │ ├── package.json │ ├── src │ ├── icons.js │ ├── index.css │ └── index.js │ ├── tsconfig.json │ └── types │ ├── icons.d.ts │ └── index.d.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── preview ├── index.html ├── package.json ├── plan.jpg ├── preview.bundle.css ├── preview.bundle.js ├── preview.css ├── preview.js └── rollup.config.js └── tsconfig.shared.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint-config-ecmascript" 4 | ], 5 | "ignorePatterns": [ 6 | "lib", 7 | "*.bundle.js", 8 | "*.d.ts" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | 14 | - name: Install Node.js 15 | uses: actions/setup-node@v4 16 | 17 | - name: Install pnpm 18 | uses: pnpm/action-setup@v4 19 | with: 20 | version: 9 21 | 22 | - name: Install dependencies 23 | run: pnpm install 24 | 25 | - name: Run linter 26 | run: pnpm lint 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # nodejs 2 | node_modules/ 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # osx 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.0.0 2 | 3 | - ⚠️ Use new `mapbox` types instead of `DefinitelyTyped` 4 | - Update `@turf` package 5 | - Update `@mapbox` package 6 | - Increase icon size for 1px for pixel perfect centering inside buttons 7 | 8 | ## 2.1.0 9 | 10 | - Update `@turf` package from alpha version to release version 11 | - Fix Vite SSR for icons 12 | - Update dependencies 13 | 14 | ## 2.0.0 15 | 16 | - ⚠️ **RulerControl**: switch from `mapboxgl.Marker` to `circle` layer to fix [#51](../../issues/56) 17 | - **StyleControl**: update types 18 | - **ImageControl**: add error hint for missing layers 19 | - Update dependencies 20 | 21 | ## 1.2.0 22 | 23 | - **StyleControl**: fix for MapBox Standard style [#55](../../pull/55) 24 | 25 | ## 1.1.0 26 | 27 | - **ImageControl**: add option for remove button [#51](../../issues/51) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mapbox Controls 2 | 3 | Some handy controls for `mapbox-gl-js` structured as monorepo. [Example of all controls](https://korywka.github.io/mapbox-controls/preview/). 4 | 5 | 6 | 7 | ## Controls list 8 | 9 | - [🧭 @mapbox-controls/compass](packages/compass) - Indicate map direction 10 | - [🏙️ @mapbox-controls/image](packages/image) - Move, scale and rotate image on a map 11 | - [🐞 @mapbox-controls/inspect](packages/inspect) - Debug map style layers and sources 12 | - [📖 @mapbox-controls/language](packages/language) - Change map language 13 | - [📏 @mapbox-controls/ruler](packages/ruler) - Measure distance between points on a map 14 | - [💅 @mapbox-controls/styles](packages/styles) - Change map style 15 | - [🏷️ @mapbox-controls/tooltip](packages/tooltip) - Display tooltip on hover 16 | - [🔍 @mapbox-controls/zoom](packages/zoom) - Zoom in and zoom out map 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mapbox-gl-controls", 3 | "private": true, 4 | "scripts": { 5 | "lint": "eslint packages/**/src/*.js", 6 | "publish": "pnpm --recursive publish", 7 | "build": "pnpm --recursive build" 8 | }, 9 | "devDependencies": { 10 | "eslint": "8.56.0", 11 | "eslint-config-ecmascript": "2.7.0", 12 | "pnpm": "9.9.0", 13 | "typescript": "5.5.4" 14 | }, 15 | "license": "MIT" 16 | } 17 | -------------------------------------------------------------------------------- /packages/compass/README.md: -------------------------------------------------------------------------------- 1 | [<< all controls](/README.md) 2 | 3 | # 🧭 @mapbox-controls/compass 4 | 5 | ![](https://github.com/korywka/mapbox-controls/assets/988471/03647bed-7a93-430b-bd49-b4d0b878734d) 6 | 7 | Control to indicate map direction. Optionally, can be hidden if map bearing iz 0. 8 | 9 | ``` 10 | npm i @mapbox-controls/compass 11 | ``` 12 | 13 | ```js 14 | import CompassControl from '@mapbox-controls/compass'; 15 | import '@mapbox-controls/compass/src/index.css'; 16 | 17 | map.addControl(new CompassControl(), 'bottom-right'); 18 | ``` 19 | 20 | ## Options 21 | 22 | ```ts 23 | export type ControlOptions = { 24 | instant?: boolean; 25 | }; 26 | ``` 27 | -------------------------------------------------------------------------------- /packages/compass/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mapbox-controls/compass", 3 | "version": "3.0.0", 4 | "description": "Indicate map direction", 5 | "type": "module", 6 | "main": "./src/index.js", 7 | "scripts": { 8 | "build": "tsc" 9 | }, 10 | "dependencies": { 11 | "@mapbox-controls/helpers": "workspace:*" 12 | }, 13 | "devDependencies": { 14 | "mapbox-gl": "catalog:" 15 | }, 16 | "peerDependencies": { 17 | "mapbox-gl": ">=1.0.0 <4.0.0" 18 | }, 19 | "types": "./types/index.d.ts", 20 | "files": [ 21 | "src", 22 | "types" 23 | ], 24 | "publishConfig": { 25 | "access": "public", 26 | "registry": "https://registry.npmjs.org/" 27 | }, 28 | "repository": "korywka/mapbox-controls", 29 | "license": "MIT" 30 | } -------------------------------------------------------------------------------- /packages/compass/src/icons.js: -------------------------------------------------------------------------------- 1 | import { parseSVG } from '@mapbox-controls/helpers'; 2 | 3 | function compass() { 4 | return parseSVG(` 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | `); 13 | } 14 | 15 | export const icons = { 16 | compass, 17 | }; 18 | -------------------------------------------------------------------------------- /packages/compass/src/index.css: -------------------------------------------------------------------------------- 1 | .mapbox-ctrl-compass { 2 | transition: .2s opacity; 3 | } 4 | 5 | .mapbox-ctrl-compass[hidden] { 6 | display: block; 7 | opacity: 0; 8 | } 9 | 10 | .mapbox-ctrl-compass button { 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | } -------------------------------------------------------------------------------- /packages/compass/src/index.js: -------------------------------------------------------------------------------- 1 | import { controlContainer, controlButton } from '@mapbox-controls/helpers'; 2 | import { icons } from './icons.js'; 3 | 4 | class CompassControl { 5 | /** 6 | * @param {import('./types').ControlOptions} options 7 | */ 8 | constructor(options = {}) { 9 | this.options = { ...options }; 10 | this.container = controlContainer('mapbox-ctrl-compass'); 11 | this.icon = icons.compass(); 12 | this.button = controlButton({ 13 | title: 'Compass', 14 | icon: this.icon, 15 | onClick: () => this.onControlButtonClick(), 16 | }); 17 | } 18 | 19 | onControlButtonClick() { 20 | if (!this.map) throw Error('map is undefined'); 21 | this.map.easeTo({ bearing: 0, pitch: 0 }); 22 | } 23 | 24 | onRotate() { 25 | if (!this.map) throw Error('map is undefined'); 26 | const angle = this.map.getBearing() * (-1); 27 | if (!this.options.instant) { 28 | this.container.hidden = angle === 0; 29 | } 30 | this.icon.style.rotate = `${angle}deg`; 31 | } 32 | 33 | /** 34 | * @param {import('mapbox-gl').Map} map 35 | * @returns {HTMLElement} 36 | */ 37 | onAdd(map) { 38 | this.map = map; 39 | if (!this.options.instant) { 40 | this.container.hidden = true; 41 | } 42 | this.container.appendChild(this.button); 43 | this.onRotate(); 44 | this.map.on('rotate', () => this.onRotate()); 45 | return this.container; 46 | } 47 | 48 | onRemove() { 49 | this.container.parentNode?.removeChild(this.container); 50 | } 51 | } 52 | 53 | export default CompassControl; 54 | -------------------------------------------------------------------------------- /packages/compass/src/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {{ 3 | * instant?: boolean 4 | * }} ControlOptions 5 | */ 6 | 7 | export {}; 8 | -------------------------------------------------------------------------------- /packages/compass/tsconfig.json: -------------------------------------------------------------------------------- 1 | ../../tsconfig.shared.json -------------------------------------------------------------------------------- /packages/compass/types/icons.d.ts: -------------------------------------------------------------------------------- 1 | export namespace icons { 2 | export { compass }; 3 | } 4 | declare function compass(): SVGElement; 5 | export {}; 6 | -------------------------------------------------------------------------------- /packages/compass/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export default CompassControl; 2 | declare class CompassControl { 3 | constructor(options?: import("./types").ControlOptions); 4 | options: { 5 | instant?: boolean; 6 | }; 7 | container: HTMLDivElement; 8 | icon: SVGElement; 9 | button: HTMLButtonElement; 10 | onControlButtonClick(): void; 11 | onRotate(): void; 12 | onAdd(map: import("mapbox-gl").Map): HTMLElement; 13 | map: import("mapbox-gl").Map | undefined; 14 | onRemove(): void; 15 | } 16 | -------------------------------------------------------------------------------- /packages/compass/types/types.d.ts: -------------------------------------------------------------------------------- 1 | export type ControlOptions = { 2 | instant?: boolean; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/helpers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mapbox-controls/helpers", 3 | "version": "3.0.0", 4 | "description": "Helpers for mapbox controls", 5 | "type": "module", 6 | "main": "./src/index.js", 7 | "scripts": { 8 | "build": "tsc" 9 | }, 10 | "devDependencies": { 11 | "@types/geojson": "catalog:", 12 | "mapbox-gl": "catalog:" 13 | }, 14 | "peerDependencies": { 15 | "mapbox-gl": ">=1.0.0 <4.0.0" 16 | }, 17 | "types": "./types/index.d.ts", 18 | "files": [ 19 | "src", 20 | "types" 21 | ], 22 | "publishConfig": { 23 | "access": "public", 24 | "registry": "https://registry.npmjs.org/" 25 | }, 26 | "repository": "korywka/mapbox-gl-controls", 27 | "license": "MIT" 28 | } -------------------------------------------------------------------------------- /packages/helpers/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create mapbox control container 3 | * @param {string} className 4 | */ 5 | export function controlContainer(className) { 6 | const container = document.createElement('div'); 7 | container.classList.add('mapboxgl-ctrl', 'mapboxgl-ctrl-group', className); 8 | return container; 9 | } 10 | 11 | /** 12 | * Create mapbox control button 13 | * @param {Object} options 14 | * @param {string=} options.title 15 | * @param {Node=} options.icon 16 | * @param {string=} options.textContent 17 | * @param {boolean=} options.disabled 18 | * @param {boolean=} options.hidden 19 | * @param {string=} options.className 20 | * @param {() => void=} options.onClick 21 | */ 22 | export function controlButton(options = {}) { 23 | const button = document.createElement('button'); 24 | button.type = 'button'; 25 | if (options.title) { 26 | button.title = options.title; 27 | } 28 | if (options.icon) { 29 | button.appendChild(options.icon); 30 | } 31 | if (options.textContent) { 32 | button.textContent = options.textContent; 33 | } 34 | if (options.disabled) { 35 | button.disabled = true; 36 | } 37 | if (options.hidden) { 38 | button.hidden = true; 39 | } 40 | if (options.className) { 41 | button.classList.add(options.className); 42 | } 43 | if (options.onClick) { 44 | button.addEventListener('click', () => { 45 | if (!options.onClick) return; 46 | options.onClick(); 47 | }); 48 | } 49 | return button; 50 | } 51 | 52 | /** 53 | * Create SVG element from string code 54 | * @param {string} string 55 | */ 56 | export function parseSVG(string) { 57 | return /** @type SVGElement */ ((new DOMParser().parseFromString(string, 'image/svg+xml')).firstChild); 58 | } 59 | -------------------------------------------------------------------------------- /packages/helpers/tsconfig.json: -------------------------------------------------------------------------------- 1 | ../../tsconfig.shared.json -------------------------------------------------------------------------------- /packages/helpers/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export function controlContainer(className: string): HTMLDivElement; 2 | export function controlButton(options?: { 3 | title?: string | undefined; 4 | icon?: Node | undefined; 5 | textContent?: string | undefined; 6 | disabled?: boolean | undefined; 7 | hidden?: boolean | undefined; 8 | className?: string | undefined; 9 | onClick?: (() => void) | undefined; 10 | }): HTMLButtonElement; 11 | export function parseSVG(string: string): SVGElement; 12 | -------------------------------------------------------------------------------- /packages/image/README.md: -------------------------------------------------------------------------------- 1 | [<< all controls](/README.md) 2 | 3 | # 🏙️ @mapbox-controls/image 4 | 5 | ![](https://github.com/korywka/mapbox-controls/assets/988471/9db0c22d-662a-43fb-90e6-1fbe4405dcc5) 6 | 7 | Control to move, scale, rotate image (raster layer) on top of a map. 8 | Very handy to represent information from the raster to geojson, for example, the contours of buildings. 9 | 10 | ``` 11 | npm i @mapbox-controls/image 12 | ``` 13 | 14 | ```js 15 | import ImageControl from '@mapbox-controls/image'; 16 | import '@mapbox-controls/image/src/index.css'; 17 | 18 | const imageControl = new ImageControl(); 19 | map.addControl(imageControl, 'bottom-right'); 20 | ``` 21 | 22 | ## Options 23 | 24 | ```ts 25 | export type ControlOptions = { 26 | removeButton?: boolean; 27 | }; 28 | ``` 29 | 30 | ## Events 31 | 32 | | event | description | 33 | | -------------- | -------------------------- | 34 | | image.select | selected new image | 35 | | image.deselect | image was deselected | 36 | | image.mode | transform mode was changed | 37 | | image.update | position was updated | 38 | | image.add | new image added | 39 | | image.remove | image removed | 40 | 41 | ## Methods 42 | 43 | Methods are useful for programmatic control (when option `invisible` is `true`): 44 | 45 | - `addFile(file: File, coordinates?: [number, number][] | undefined): Promise;` - add new image by file. raster id is returned 46 | - `addUrl(url: string, coordinates?: [number, number][] | undefined): Promise;` - add new image by url. raster id is returned 47 | - `setLock: (id: string, isLocked: boolean) => void;` - lock or unlock image. locked image can't be selected 48 | - `removeRaster: () => void;` - removes selected raster from the map 49 | 50 | If image was added without `coordinates` parameter, the image is scaled down to be fully visible and placed at the center of the viewport. 51 | 52 | Other methods may help to use this control without buttons, these methods are described in type definitions `.d.ts`. 53 | 54 | ## Change paint properties 55 | 56 | Paint properties can be changed dynamically. 57 | Below is an example how to control image opacity by slider (full implementation is available in [preview](/preview/preview.js)). 58 | 59 | ```js 60 | map.on('image.select', ({ id }) => { 61 | const rasterLayerId = image.rasters[id].rasterLayer.id; 62 | const range = document.createElement('input'); 63 | range.style.position = 'absolute'; 64 | range.style.left = '50%'; 65 | range.style.transform = 'translateX(-50%)'; 66 | range.style.bottom = '16px'; 67 | range.type = 'range'; 68 | range.min = 0; 69 | range.step = 0.05; 70 | range.max = 1; 71 | range.value = map.getPaintProperty(rasterLayerId, 'raster-opacity'); 72 | range.addEventListener('input', () => { 73 | map.setPaintProperty(rasterLayerId, 'raster-opacity', Number(range.value)); 74 | }); 75 | document.body.appendChild(range); 76 | map.once('image.deselect', () => { 77 | document.body.removeChild(range); 78 | }); 79 | }); 80 | ``` 81 | -------------------------------------------------------------------------------- /packages/image/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mapbox-controls/image", 3 | "version": "3.0.0", 4 | "description": "Move, scale and rotate image on a map", 5 | "type": "module", 6 | "main": "./src/index.js", 7 | "scripts": { 8 | "build": "tsc" 9 | }, 10 | "dependencies": { 11 | "@mapbox-controls/helpers": "workspace:*", 12 | "@turf/bearing": "7.1.0", 13 | "@turf/centroid": "7.1.0", 14 | "@turf/helpers": "7.1.0", 15 | "@turf/rhumb-bearing": "7.1.0", 16 | "@turf/rhumb-distance": "7.1.0", 17 | "@turf/transform-rotate": "7.1.0", 18 | "@turf/transform-scale": "7.1.0", 19 | "@turf/transform-translate": "7.1.0" 20 | }, 21 | "devDependencies": { 22 | "@types/geojson": "catalog:", 23 | "mapbox-gl": "catalog:" 24 | }, 25 | "peerDependencies": { 26 | "mapbox-gl": ">=1.0.0 <4.0.0" 27 | }, 28 | "types": "./types/index.d.ts", 29 | "files": [ 30 | "src", 31 | "types" 32 | ], 33 | "publishConfig": { 34 | "access": "public", 35 | "registry": "https://registry.npmjs.org/" 36 | }, 37 | "repository": "korywka/mapbox-controls", 38 | "license": "MIT" 39 | } -------------------------------------------------------------------------------- /packages/image/src/center-position.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {HTMLImageElement} image 3 | * @param {import('mapbox-gl').Map} map 4 | * @param {number} padding 5 | */ 6 | export function centerPosition(image, map, padding = 20) { 7 | const canvas = map.getCanvas(); 8 | const canvasWidth = canvas.offsetWidth; 9 | const canvasHeight = canvas.offsetHeight; 10 | const maxWidth = canvasWidth - padding * 2; 11 | const maxHeight = canvasHeight - padding * 2; 12 | const ratio = Math.min(maxWidth / image.width, maxHeight / image.height); 13 | const scaleWidth = image.width * ratio; 14 | const scaleHeight = image.height * ratio; 15 | /** @type {import('./types').RasterCoordinates} */ 16 | const position = [ 17 | [(canvasWidth - scaleWidth) / 2, (canvasHeight - scaleHeight) / 2], // left top 18 | [(canvasWidth + scaleWidth) / 2, (canvasHeight - scaleHeight) / 2], // right top 19 | [(canvasWidth + scaleWidth) / 2, (canvasHeight + scaleHeight) / 2], // right bottom 20 | [(canvasWidth - scaleWidth) / 2, (canvasHeight + scaleHeight) / 2], // left bottom 21 | ]; 22 | 23 | /** 24 | * reset pitch for correct projection 25 | */ 26 | map.setPitch(0); 27 | 28 | return /** @type {import('./types').RasterCoordinates} */ ([ 29 | map.unproject(position[0]).toArray(), 30 | map.unproject(position[1]).toArray(), 31 | map.unproject(position[2]).toArray(), 32 | map.unproject(position[3]).toArray(), 33 | ]); 34 | } 35 | 36 | -------------------------------------------------------------------------------- /packages/image/src/file.js: -------------------------------------------------------------------------------- 1 | export function createFileInput() { 2 | const node = document.createElement('input'); 3 | node.type = 'file'; 4 | node.accept = '.jpg, .jpeg, .png'; 5 | node.multiple = false; 6 | return node; 7 | } 8 | 9 | /** 10 | * @param {File} file 11 | * @returns {Promise} 12 | */ 13 | export function readFile(file) { 14 | return new Promise((resolve, reject) => { 15 | const url = URL.createObjectURL(file); 16 | const node = document.createElement('img'); 17 | node.onload = () => { 18 | resolve(node); 19 | }; 20 | node.onerror = reject; 21 | node.src = url; 22 | }); 23 | } 24 | 25 | /** 26 | * @param {string} url 27 | * @return {Promise} 28 | */ 29 | export function readUrl(url) { 30 | return new Promise(((resolve, reject) => { 31 | const node = document.createElement('img'); 32 | node.onload = () => { 33 | resolve(node); 34 | }; 35 | node.onerror = reject; 36 | node.src = url; 37 | })); 38 | } 39 | -------------------------------------------------------------------------------- /packages/image/src/icons.js: -------------------------------------------------------------------------------- 1 | import { parseSVG } from '@mapbox-controls/helpers'; 2 | 3 | function image() { 4 | return parseSVG(` 5 | 6 | 7 | 8 | 9 | `); 10 | } 11 | 12 | function move() { 13 | return parseSVG(` 14 | 15 | 16 | 17 | 18 | `); 19 | } 20 | 21 | function scale() { 22 | return parseSVG(` 23 | 24 | 25 | 26 | 27 | `); 28 | } 29 | 30 | function rotate() { 31 | return parseSVG(` 32 | 33 | 34 | 35 | 36 | `); 37 | } 38 | 39 | function remove() { 40 | return parseSVG(` 41 | 42 | 43 | 44 | `); 45 | } 46 | 47 | export const icons = { 48 | move, 49 | image, 50 | scale, 51 | rotate, 52 | remove, 53 | }; 54 | -------------------------------------------------------------------------------- /packages/image/src/index.css: -------------------------------------------------------------------------------- 1 | .mapbox-ctrl-image button { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | color: #333; 6 | cursor: pointer; 7 | } 8 | 9 | .mapbox-ctrl-image button[hidden] { 10 | display: none; 11 | } 12 | 13 | .mapbox-ctrl-image button.-active { 14 | color: #4264fb; 15 | } 16 | 17 | .mapbox-ctrl-image button:disabled { 18 | opacity: 0.5; 19 | } 20 | 21 | .mapbox-ctrl-image-add { 22 | position: relative; 23 | display: contents; 24 | } 25 | 26 | .mapbox-ctrl-image input[type="file"] { 27 | opacity: 0; 28 | position: absolute; 29 | left: 0; 30 | top: 0; 31 | width: 1px; 32 | height: 1px; 33 | } -------------------------------------------------------------------------------- /packages/image/src/index.js: -------------------------------------------------------------------------------- 1 | import { controlButton, controlContainer } from '@mapbox-controls/helpers'; 2 | import { icons } from './icons.js'; 3 | import { Raster } from './raster.js'; 4 | import { Move } from './modes/move.js'; 5 | import { Scale } from './modes/scale.js'; 6 | import { Rotate } from './modes/rotate.js'; 7 | import { centerPosition } from './center-position.js'; 8 | import { createFileInput, readFile, readUrl } from './file.js'; 9 | 10 | class ImageControl { 11 | /** @param {import('./types').ControlOptions} options */ 12 | constructor(options = {}) { 13 | this.container = controlContainer('mapbox-ctrl-image'); 14 | this.fileInput = createFileInput(); 15 | this.buttonAdd = controlButton({ 16 | title: 'Add image', 17 | icon: icons.image(), 18 | className: 'mapbox-ctrl-image-add', 19 | onClick: () => this.fileInput.click(), 20 | }); 21 | this.buttonMove = controlButton({ 22 | disabled: true, 23 | title: 'Move image', 24 | icon: icons.move(), 25 | onClick: () => this.setMode('move'), 26 | }); 27 | this.buttonScale = controlButton({ 28 | disabled: true, 29 | title: 'Scale image', 30 | icon: icons.scale(), 31 | onClick: () => this.setMode('scale'), 32 | }); 33 | this.buttonRotate = controlButton({ 34 | disabled: true, 35 | title: 'Rotate image', 36 | icon: icons.rotate(), 37 | onClick: () => this.setMode('rotate'), 38 | }); 39 | if (options.removeButton) { 40 | this.buttonRemove = controlButton({ 41 | hidden: true, 42 | title: 'Remove image', 43 | icon: icons.remove(), 44 | onClick: () => this.removeRaster(), 45 | }); 46 | } 47 | /** @type {Record} */ 48 | this.rasters = {}; 49 | /** @type {Raster | null} */ 50 | this.currentRaster = null; 51 | /** @type {Move | Scale | Rotate | null} */ 52 | this.currentMode = null; 53 | } 54 | 55 | /** 56 | * @param {File} file 57 | * @param {import('./types').RasterCoordinates=} coordinates 58 | */ 59 | async addFile(file, coordinates) { 60 | const image = await readFile(file); 61 | const id = this.addImage(image, coordinates); 62 | return id; 63 | } 64 | 65 | /** 66 | * @param {string} url 67 | * @param {import('./types').RasterCoordinates=} coordinates 68 | */ 69 | async addUrl(url, coordinates) { 70 | const image = await readUrl(url); 71 | const id = this.addImage(image, coordinates); 72 | return id; 73 | } 74 | 75 | /** 76 | * @param {HTMLImageElement} image 77 | * @param {import('./types').RasterCoordinates=} coordinates 78 | */ 79 | async addImage(image, coordinates) { 80 | if (!this.map) throw Error('map is undefined'); 81 | const position = coordinates ?? centerPosition(image, this.map); 82 | const raster = new Raster(image, position); 83 | this.addRaster(raster); 84 | return raster.id; 85 | } 86 | 87 | /** 88 | * @param {Raster} raster 89 | */ 90 | addRaster(raster) { 91 | if (!this.map) throw Error('map is undefined'); 92 | this.rasters[raster.id] = raster; 93 | this.map.addSource(raster.rasterSource.id, raster.rasterSource.source); 94 | this.map.addSource(raster.polygonSource.id, raster.polygonSource.source); 95 | this.map.addSource(raster.pointsSource.id, raster.pointsSource.source); 96 | this.map.addLayer(raster.rasterLayer); 97 | this.map.addLayer(raster.fillLayer); 98 | // @ts-ignore 99 | this.map.fire('image.add', { id: raster.id }); 100 | } 101 | 102 | removeRaster() { 103 | if (!this.map) throw Error('map is undefined'); 104 | if (!this.currentRaster) throw Error('no raster is selected'); 105 | const rasterId = this.currentRaster.id; 106 | const raster = this.rasters[rasterId]; 107 | this.deselectRaster(); 108 | delete this.rasters[rasterId]; 109 | this.map.removeLayer(raster.rasterLayer.id); 110 | this.map.removeLayer(raster.fillLayer.id); 111 | this.map.removeSource(raster.rasterSource.id); 112 | this.map.removeSource(raster.polygonSource.id); 113 | this.map.removeSource(raster.pointsSource.id); 114 | // @ts-ignore 115 | this.map.fire('image.remove', { id: raster.id }); 116 | } 117 | 118 | /** 119 | * @param {string} id 120 | */ 121 | selectRaster(id) { 122 | if (!this.map) throw Error('map is undefined'); 123 | this.deselectRaster(); 124 | const raster = this.rasters[id]; 125 | if (raster.locked) return; 126 | this.currentRaster = raster; 127 | this.map.addLayer(this.currentRaster.contourLayer); 128 | this.buttonMove.disabled = false; 129 | this.buttonScale.disabled = false; 130 | this.buttonRotate.disabled = false; 131 | if (this.buttonRemove) { 132 | this.buttonAdd.hidden = true; 133 | this.buttonRemove.hidden = false; 134 | } 135 | // @ts-ignore 136 | this.map.fire('image.select', { id: this.currentRaster.id }); 137 | } 138 | 139 | deselectRaster() { 140 | if (!this.map) throw Error('map is undefined'); 141 | if (!this.currentRaster) return; 142 | this.map.removeLayer(this.currentRaster.contourLayer.id); 143 | // @ts-ignore 144 | this.map.fire('image.deselect', { id: this.currentRaster.id }); 145 | this.setMode(null); 146 | this.currentRaster = null; 147 | this.buttonMove.disabled = true; 148 | this.buttonScale.disabled = true; 149 | this.buttonRotate.disabled = true; 150 | if (this.buttonRemove) { 151 | this.buttonAdd.hidden = false; 152 | this.buttonRemove.hidden = true; 153 | } 154 | } 155 | 156 | /** 157 | * @param {'move' | 'scale' | 'rotate' | null} mode 158 | */ 159 | setMode(mode) { 160 | if (!this.map) throw Error('map is undefined'); 161 | if (!this.currentRaster) throw Error('no raster is selected'); 162 | if (this.currentMode) { 163 | const currentId = this.currentMode.id; 164 | this.buttonMove.classList.remove('-active'); 165 | this.buttonScale.classList.remove('-active'); 166 | this.buttonRotate.classList.remove('-active'); 167 | this.currentMode.destroy(); 168 | this.currentMode = null; 169 | // @ts-ignore 170 | this.map.fire('image.mode', { mode: this.currentMode }); 171 | // click on active button just deactivates current mode 172 | if (currentId === mode) return; 173 | } 174 | if (mode === 'move') { 175 | this.buttonMove.classList.add('-active'); 176 | this.currentMode = new Move(this.map, this.currentRaster, (coordinates) => { 177 | this.updateCoordinates(coordinates); 178 | }); 179 | } 180 | if (mode === 'scale') { 181 | this.buttonScale.classList.add('-active'); 182 | this.currentMode = new Scale(this.map, this.currentRaster, (coordinates) => { 183 | this.updateCoordinates(coordinates); 184 | }); 185 | } 186 | if (mode === 'rotate') { 187 | this.buttonRotate.classList.add('-active'); 188 | this.currentMode = new Rotate(this.map, this.currentRaster, (coordinates) => { 189 | this.updateCoordinates(coordinates); 190 | }); 191 | } 192 | if (this.currentMode) { 193 | // @ts-ignore 194 | this.map.fire('image.mode', { mode: this.currentMode.id }); 195 | } 196 | } 197 | 198 | /** 199 | * @typedef {import('mapbox-gl').ImageSource} ImageSource 200 | * @typedef {import('mapbox-gl').GeoJSONSource} GeoJSONSource 201 | * @param {import('./types').RasterCoordinates} coordinates 202 | */ 203 | updateCoordinates(coordinates) { 204 | if (!this.map) throw Error('map is undefined'); 205 | if (!this.currentRaster) throw Error('no raster is selected'); 206 | const raster = this.currentRaster; 207 | raster.coordinates = coordinates; 208 | const rasterSource = /** @type {ImageSource} */ (this.map.getSource(raster.rasterSource.id)); 209 | const polygonSource = /** @type {GeoJSONSource} */ (this.map.getSource(raster.polygonSource.id)); 210 | const pointsSource = /** @type {GeoJSONSource} */ (this.map.getSource(raster.pointsSource.id)); 211 | rasterSource.setCoordinates(raster.coordinates); 212 | polygonSource.setData(raster.polygonSource.source.data); 213 | pointsSource.setData(raster.pointsSource.source.data); 214 | // @ts-ignore 215 | this.map.fire('image.update', { coordinates }); 216 | } 217 | 218 | /** 219 | * @param {import('mapbox-gl').MapMouseEvent} event 220 | */ 221 | onMapClick = (event) => { 222 | if (!this.map) throw Error('map is undefined'); 223 | const layersId = Object.values(this.rasters).map((i) => i.fillLayer.id); 224 | // sometimes layers are removed from the map without destroying the control, e.g. style was changed 225 | const errorLayerId = layersId.find((id) => { 226 | return !this.map?.getLayer(id); 227 | }); 228 | if (errorLayerId) { 229 | return; 230 | } 231 | const features = this.map.queryRenderedFeatures(event.point, { layers: layersId }); 232 | if (features[0]) { 233 | /** @type {string} */ 234 | const id = features[0].properties?.id; 235 | if (!id) throw Error('id property is undefined'); 236 | this.selectRaster(id); 237 | return; 238 | } 239 | if (this.currentRaster) { 240 | // add extra padding to not deselect raster on it's knobs layer click 241 | let padding = 0; 242 | if (typeof this.currentRaster.knobsLayer.paint?.['circle-radius'] === 'number') { 243 | padding = this.currentRaster.knobsLayer.paint['circle-radius'] * 2; 244 | } 245 | const { x, y } = event.point; 246 | /** @type {[[number, number], [number, number]]} */ 247 | const bbox = [[x - padding, y - padding], [x + padding, y + padding]]; 248 | const features = this.map.queryRenderedFeatures(bbox, { layers: layersId }); 249 | if (!features.length) { 250 | this.deselectRaster(); 251 | } 252 | } 253 | }; 254 | 255 | /** 256 | * @param {string} id 257 | * @param {boolean} isLocked 258 | */ 259 | setLock = (id, isLocked) => { 260 | this.rasters[id].locked = isLocked; 261 | if (this.currentRaster?.id === id && isLocked) { 262 | this.deselectRaster(); 263 | } 264 | }; 265 | 266 | /** 267 | * @param {import('mapbox-gl').Map} map 268 | * @returns {HTMLElement} 269 | */ 270 | onAdd(map) { 271 | this.map = map; 272 | this.container.appendChild(this.fileInput); 273 | this.container.appendChild(this.buttonAdd); 274 | if (this.buttonRemove) { 275 | this.container.appendChild(this.buttonRemove); 276 | } 277 | this.container.appendChild(this.buttonMove); 278 | this.container.appendChild(this.buttonScale); 279 | this.container.appendChild(this.buttonRotate); 280 | this.fileInput.addEventListener('change', async () => { 281 | const file = this.fileInput.files?.[0]; 282 | if (!file) return; 283 | await this.addFile(file); 284 | }); 285 | this.map.on('click', this.onMapClick); 286 | return this.container; 287 | } 288 | 289 | onRemove() { 290 | this.map?.off('click', this.onMapClick); 291 | this.container.parentNode?.removeChild(this.container); 292 | } 293 | } 294 | 295 | export default ImageControl; 296 | -------------------------------------------------------------------------------- /packages/image/src/modes/move.js: -------------------------------------------------------------------------------- 1 | import rhumbBearing from '@turf/rhumb-bearing'; 2 | import rhumbDistance from '@turf/rhumb-distance'; 3 | import transformTranslate from '@turf/transform-translate'; 4 | 5 | export class Move { 6 | /** 7 | * @param {import('mapbox-gl').Map} map 8 | * @param {import('../raster').Raster} raster 9 | * @param {(coordinates: import('../types').RasterCoordinates) => void} onUpdate 10 | */ 11 | constructor(map, raster, onUpdate) { 12 | this.map = map; 13 | this.raster = raster; 14 | this.onUpdate = onUpdate; 15 | /** @type { [number, number] | null } */ 16 | this.prevPosition = null; 17 | this.map.on('mouseenter', this.raster.fillLayer.id, this.onPointerEnter); 18 | this.map.on('mouseleave', this.raster.fillLayer.id, this.onPointerLeave); 19 | this.map.on('mousedown', this.raster.fillLayer.id, this.onPointerDown); 20 | } 21 | 22 | get id() { 23 | return 'move'; 24 | } 25 | 26 | onPointerEnter = () => { 27 | this.map.getCanvas().style.cursor = 'move'; 28 | }; 29 | 30 | onPointerLeave = () => { 31 | this.map.getCanvas().style.cursor = ''; 32 | }; 33 | 34 | /** 35 | * @param {import('mapbox-gl').MapMouseEvent} event 36 | */ 37 | onPointerDown = (event) => { 38 | event.preventDefault(); 39 | this.prevPosition = [event.lngLat.lng, event.lngLat.lat]; 40 | this.map.on('mousemove', this.onPointerMove); 41 | this.map.getCanvas().style.cursor = 'grabbing'; 42 | document.addEventListener('pointerup', this.onPointerUp, { once: true }); 43 | }; 44 | 45 | /** 46 | * @param {import('mapbox-gl').MapMouseEvent} event 47 | */ 48 | onPointerMove = (event) => { 49 | if (!this.prevPosition) throw Error('previous position is undefined'); 50 | /** @type {[number, number]} */ 51 | const currentPosition = [event.lngLat.lng, event.lngLat.lat]; 52 | const bearingBetween = rhumbBearing(this.prevPosition, currentPosition); 53 | const distanceBetween = rhumbDistance(this.prevPosition, currentPosition); 54 | const geojson = this.raster.polygonSource.source.data; 55 | const transformed = transformTranslate(geojson, distanceBetween, bearingBetween); 56 | const transformedCoordinates = transformed.geometry.coordinates[0]; 57 | // remove closing 5th coordinate from polygon 58 | const position = /** @type {import('../types').RasterCoordinates} */ (transformedCoordinates.slice(0, 4)); 59 | this.onUpdate(position); 60 | this.prevPosition = currentPosition; 61 | }; 62 | 63 | onPointerUp = () => { 64 | this.map.getCanvas().style.cursor = 'move'; 65 | this.map.off('mousemove', this.onPointerMove); 66 | }; 67 | 68 | destroy() { 69 | this.prevPosition = null; 70 | this.map.getCanvas().style.cursor = ''; 71 | this.map.off('mouseenter', this.raster.fillLayer.id, this.onPointerEnter); 72 | this.map.off('mouseleave', this.raster.fillLayer.id, this.onPointerLeave); 73 | this.map.off('mousedown', this.raster.fillLayer.id, this.onPointerDown); 74 | this.map.off('mousemove', this.onPointerMove); 75 | document.removeEventListener('pointerup', this.onPointerUp); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/image/src/modes/rotate.js: -------------------------------------------------------------------------------- 1 | import bearing from '@turf/bearing'; 2 | import centroid from '@turf/centroid'; 3 | import { bearingToAzimuth } from '@turf/helpers'; 4 | import transformRotate from '@turf/transform-rotate'; 5 | 6 | export class Rotate { 7 | /** 8 | * @param {import('mapbox-gl').Map} map 9 | * @param {import('../raster').Raster} raster 10 | * @param {(coordinates: import('../types').RasterCoordinates) => void} onUpdate 11 | */ 12 | constructor(map, raster, onUpdate) { 13 | this.map = map; 14 | this.raster = raster; 15 | this.onUpdate = onUpdate; 16 | /** @type { [number, number] | null } */ 17 | this.centroid = null; 18 | /** @type { [number, number] | null } */ 19 | this.startPoint = null; 20 | this.map.addLayer(this.raster.knobsLayer); 21 | this.map.on('mouseenter', this.raster.knobsLayer.id, this.onPointerEnter); 22 | this.map.on('mouseleave', this.raster.knobsLayer.id, this.onPointerLeave); 23 | this.map.on('mousedown', this.raster.knobsLayer.id, this.onPointerDown); 24 | } 25 | 26 | get id() { 27 | return 'rotate'; 28 | } 29 | 30 | onPointerEnter = () => { 31 | this.map.getCanvas().style.cursor = 'pointer'; 32 | }; 33 | 34 | onPointerLeave = () => { 35 | this.map.getCanvas().style.cursor = ''; 36 | }; 37 | 38 | /** 39 | * @param {import('mapbox-gl').MapMouseEvent} event 40 | */ 41 | onPointerDown = (event) => { 42 | event.preventDefault(); 43 | const geojson = this.raster.polygonSource.source.data; 44 | this.centroid = /** @type {[number, number]} */ (centroid(geojson).geometry.coordinates); 45 | this.startPoint = [event.lngLat.lng, event.lngLat.lat]; 46 | this.map.on('mousemove', this.onPointerMove); 47 | document.addEventListener('pointerup', this.onPointerUp, { once: true }); 48 | }; 49 | 50 | /** 51 | * @param {import('mapbox-gl').MapMouseEvent} event 52 | */ 53 | onPointerMove = (event) => { 54 | if (!this.centroid) throw Error('centroid is undefined'); 55 | if (!this.startPoint) throw Error('previous position is undefined'); 56 | /** @type {[number, number]} */ 57 | const currentPosition = [event.lngLat.lng, event.lngLat.lat]; 58 | const azimuthA = bearingToAzimuth(bearing(this.startPoint, this.centroid)); 59 | const azimuthB = bearingToAzimuth(bearing(currentPosition, this.centroid)); 60 | const delta = azimuthB - azimuthA; 61 | const geojson = this.raster.polygonSource.source.data; 62 | const transformed = transformRotate(geojson, delta); 63 | const transformedCoordinates = transformed.geometry.coordinates[0]; 64 | // remove closing 5th coordinate from polygon 65 | const position = /** @type {import('../types').RasterCoordinates} */ (transformedCoordinates.slice(0, 4)); 66 | this.onUpdate(position); 67 | this.startPoint = currentPosition; 68 | }; 69 | 70 | onPointerUp = () => { 71 | this.map.getCanvas().style.cursor = 'pointer'; 72 | this.map.off('mousemove', this.onPointerMove); 73 | }; 74 | 75 | destroy() { 76 | this.centroid = null; 77 | this.startPoint = null; 78 | this.map.off('mouseenter', this.raster.knobsLayer.id, this.onPointerEnter); 79 | this.map.off('mouseleave', this.raster.knobsLayer.id, this.onPointerLeave); 80 | this.map.off('mousedown', this.raster.knobsLayer.id, this.onPointerDown); 81 | this.map.off('mousemove', this.onPointerMove); 82 | this.map.removeLayer(this.raster.knobsLayer.id); 83 | document.removeEventListener('pointerup', this.onPointerUp); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/image/src/modes/scale.js: -------------------------------------------------------------------------------- 1 | import rhumbDistance from '@turf/rhumb-distance'; 2 | import transformScale from '@turf/transform-scale'; 3 | 4 | export class Scale { 5 | /** 6 | * @param {import('mapbox-gl').Map} map 7 | * @param {import('../raster').Raster} raster 8 | * @param {(coordinates: import('../types').RasterCoordinates) => void} onUpdate 9 | */ 10 | constructor(map, raster, onUpdate) { 11 | this.map = map; 12 | this.raster = raster; 13 | this.onUpdate = onUpdate; 14 | /** @type { number | null } */ 15 | this.knobIndex = null; 16 | this.map.addLayer(this.raster.knobsLayer); 17 | this.map.on('mouseenter', this.raster.knobsLayer.id, this.onPointerEnter); 18 | this.map.on('mouseleave', this.raster.knobsLayer.id, this.onPointerLeave); 19 | this.map.on('mousedown', this.raster.knobsLayer.id, this.onPointerDown); 20 | } 21 | 22 | get id() { 23 | return 'scale'; 24 | } 25 | 26 | /** 27 | * @param {import('mapbox-gl').MapMouseEvent} event 28 | */ 29 | onPointerEnter = (event) => { 30 | if (!event.features) return; 31 | this.map.getCanvas().style.cursor = 'pointer'; 32 | }; 33 | 34 | onPointerLeave = () => { 35 | this.map.getCanvas().style.cursor = ''; 36 | }; 37 | 38 | /** 39 | * @param {import('mapbox-gl').MapMouseEvent} event 40 | */ 41 | onPointerDown = (event) => { 42 | event.preventDefault(); 43 | if (!event.features) return; 44 | this.map.getCanvas().style.cursor = 'grabbing'; 45 | this.knobIndex = event.features[0].properties?.index; 46 | this.map.on('mousemove', this.onPointerMove); 47 | document.addEventListener('pointerup', this.onPointerUp, { once: true }); 48 | }; 49 | 50 | /** 51 | * @param {import('mapbox-gl').MapMouseEvent} event 52 | */ 53 | onPointerMove = (event) => { 54 | if (typeof this.knobIndex !== 'number') throw Error('knob index is undefined'); 55 | const index0 = (this.knobIndex + 2) % 4; 56 | const point0 = this.raster.coordinates[index0]; 57 | const pointA = this.raster.coordinates[this.knobIndex]; 58 | const pointB = [event.lngLat.lng, event.lngLat.lat]; 59 | const distA0 = rhumbDistance(pointA, point0); 60 | const distB0 = rhumbDistance(pointB, point0); 61 | const scale = distB0 / distA0; 62 | const geojson = this.raster.polygonSource.source.data; 63 | const transformed = transformScale(geojson, scale, { origin: point0 }); 64 | const transformedCoordinates = transformed.geometry.coordinates[0]; 65 | // remove closing 5th coordinate from polygon 66 | const position = /** @type {import('../types').RasterCoordinates} */ (transformedCoordinates.slice(0, 4)); 67 | this.onUpdate(position); 68 | }; 69 | 70 | onPointerUp = () => { 71 | this.map.getCanvas().style.cursor = ''; 72 | this.map.off('mousemove', this.onPointerMove); 73 | }; 74 | 75 | destroy() { 76 | this.map.off('mouseenter', this.raster.knobsLayer.id, this.onPointerEnter); 77 | this.map.off('mouseleave', this.raster.knobsLayer.id, this.onPointerLeave); 78 | this.map.off('mousedown', this.raster.knobsLayer.id, this.onPointerDown); 79 | this.map.off('mousemove', this.onPointerMove); 80 | this.map.removeLayer(this.raster.knobsLayer.id); 81 | document.removeEventListener('pointerup', this.onPointerUp); 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /packages/image/src/raster.js: -------------------------------------------------------------------------------- 1 | import { featureCollection, polygon, point } from '@turf/helpers'; 2 | 3 | export class Raster { 4 | /** 5 | * @param {HTMLImageElement} image 6 | * @param {import('./types').RasterCoordinates} coordinates 7 | */ 8 | constructor(image, coordinates) { 9 | this.src = image.src; 10 | this.width = image.width; 11 | this.height = image.height; 12 | this.coordinates = coordinates; 13 | this.locked = false; 14 | } 15 | 16 | get id() { 17 | const id = this.src.split('/').pop(); 18 | if (!id) throw Error(`can't get id from '${this.src}' source`); 19 | return id; 20 | } 21 | 22 | /** 23 | * @type {{ 24 | * id: string, 25 | * source: import('mapbox-gl').ImageSourceSpecification 26 | * }} 27 | */ 28 | get rasterSource() { 29 | return { 30 | id: `$raster:${this.id}`, 31 | source: { 32 | type: 'image', 33 | url: this.src, 34 | coordinates: this.coordinates, 35 | }, 36 | }; 37 | } 38 | 39 | /** 40 | * @type {{ 41 | * id: string, 42 | * source: { 43 | * type: 'geojson', 44 | * data: import('geojson').Feature 45 | * } 46 | * }} 47 | */ 48 | get polygonSource() { 49 | const feature = polygon([[...this.coordinates, this.coordinates[0]]], { id: this.id }); 50 | return { 51 | id: `$polygon:${this.id}`, 52 | source: { 53 | type: 'geojson', 54 | data: feature, 55 | }, 56 | }; 57 | } 58 | 59 | /** 60 | * @type {{ 61 | * id: string, 62 | * source: { 63 | * type: 'geojson', 64 | * data: import('geojson').FeatureCollection 65 | * } 66 | * }} 67 | */ 68 | get pointsSource() { 69 | const features = this.coordinates.map((coordinate, index) => point(coordinate, { index })); 70 | return { 71 | id: `$points:${this.id}`, 72 | source: { 73 | type: 'geojson', 74 | data: featureCollection(features), 75 | }, 76 | }; 77 | } 78 | 79 | /** @type {import('mapbox-gl').RasterLayerSpecification} */ 80 | get rasterLayer() { 81 | return { 82 | id: `$raster:${this.id}`, 83 | type: 'raster', 84 | source: this.rasterSource.id, 85 | paint: { 86 | 'raster-fade-duration': 0, 87 | 'raster-opacity': 0.5, 88 | }, 89 | }; 90 | } 91 | 92 | /** @type {import('mapbox-gl').FillLayerSpecification} */ 93 | get fillLayer() { 94 | return { 95 | id: `$fill:${this.id}`, 96 | type: 'fill', 97 | source: this.polygonSource.id, 98 | paint: { 99 | 'fill-opacity': 0, 100 | }, 101 | }; 102 | } 103 | 104 | /** @type {import('mapbox-gl').LineLayerSpecification} */ 105 | get contourLayer() { 106 | return { 107 | id: `$contour:${this.id}`, 108 | type: 'line', 109 | source: this.polygonSource.id, 110 | layout: { 111 | 'line-cap': 'round', 112 | 'line-join': 'round', 113 | }, 114 | paint: { 115 | 'line-dasharray': [0.2, 2], 116 | 'line-color': 'rgb(61, 90, 254)', 117 | 'line-width': [ 118 | 'interpolate', ['linear'], ['zoom'], 119 | 12, 1, 120 | 14, 2, 121 | ], 122 | }, 123 | }; 124 | } 125 | 126 | /** @type {import('mapbox-gl').CircleLayerSpecification} */ 127 | get knobsLayer() { 128 | return { 129 | id: `$knobs:${this.id}`, 130 | type: 'circle', 131 | source: this.pointsSource.id, 132 | paint: { 133 | 'circle-radius': 5, 134 | 'circle-color': 'rgb(61, 90, 254)', 135 | 'circle-stroke-width': 3, 136 | 'circle-stroke-color': '#fff', 137 | }, 138 | }; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /packages/image/src/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {{ 3 | * removeButton?: boolean 4 | * }} ControlOptions 5 | */ 6 | 7 | /** @typedef {[[number, number],[number, number],[number, number],[number, number]]} RasterCoordinates */ 8 | 9 | export {}; 10 | -------------------------------------------------------------------------------- /packages/image/tsconfig.json: -------------------------------------------------------------------------------- 1 | ../../tsconfig.shared.json -------------------------------------------------------------------------------- /packages/image/types/center-position.d.ts: -------------------------------------------------------------------------------- 1 | export function centerPosition(image: HTMLImageElement, map: import("mapbox-gl").Map, padding?: number): import("./types").RasterCoordinates; 2 | -------------------------------------------------------------------------------- /packages/image/types/file.d.ts: -------------------------------------------------------------------------------- 1 | export function createFileInput(): HTMLInputElement; 2 | export function readFile(file: File): Promise; 3 | export function readUrl(url: string): Promise; 4 | -------------------------------------------------------------------------------- /packages/image/types/icons.d.ts: -------------------------------------------------------------------------------- 1 | export namespace icons { 2 | export { move }; 3 | export { image }; 4 | export { scale }; 5 | export { rotate }; 6 | export { remove }; 7 | } 8 | declare function move(): SVGElement; 9 | declare function image(): SVGElement; 10 | declare function scale(): SVGElement; 11 | declare function rotate(): SVGElement; 12 | declare function remove(): SVGElement; 13 | export {}; 14 | -------------------------------------------------------------------------------- /packages/image/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export default ImageControl; 2 | declare class ImageControl { 3 | constructor(options?: import("./types").ControlOptions); 4 | container: HTMLDivElement; 5 | fileInput: HTMLInputElement; 6 | buttonAdd: HTMLButtonElement; 7 | buttonMove: HTMLButtonElement; 8 | buttonScale: HTMLButtonElement; 9 | buttonRotate: HTMLButtonElement; 10 | buttonRemove: HTMLButtonElement | undefined; 11 | rasters: Record; 12 | currentRaster: Raster | null; 13 | currentMode: Move | Scale | Rotate | null; 14 | addFile(file: File, coordinates?: import("./types").RasterCoordinates | undefined): Promise; 15 | addUrl(url: string, coordinates?: import("./types").RasterCoordinates | undefined): Promise; 16 | addImage(image: HTMLImageElement, coordinates?: import("./types").RasterCoordinates | undefined): Promise; 17 | addRaster(raster: Raster): void; 18 | removeRaster(): void; 19 | selectRaster(id: string): void; 20 | deselectRaster(): void; 21 | setMode(mode: "move" | "scale" | "rotate" | null): void; 22 | updateCoordinates(coordinates: import("./types").RasterCoordinates): void; 23 | onMapClick: (event: import("mapbox-gl").MapMouseEvent) => void; 24 | setLock: (id: string, isLocked: boolean) => void; 25 | onAdd(map: import("mapbox-gl").Map): HTMLElement; 26 | map: import("mapbox-gl").Map | undefined; 27 | onRemove(): void; 28 | } 29 | import { Raster } from './raster.js'; 30 | import { Move } from './modes/move.js'; 31 | import { Scale } from './modes/scale.js'; 32 | import { Rotate } from './modes/rotate.js'; 33 | -------------------------------------------------------------------------------- /packages/image/types/modes/move.d.ts: -------------------------------------------------------------------------------- 1 | export class Move { 2 | constructor(map: import("mapbox-gl").Map, raster: import("../raster").Raster, onUpdate: (coordinates: import("../types").RasterCoordinates) => void); 3 | map: import("mapbox-gl").Map; 4 | raster: import("../raster").Raster; 5 | onUpdate: (coordinates: import("../types").RasterCoordinates) => void; 6 | prevPosition: [number, number] | null; 7 | get id(): string; 8 | onPointerEnter: () => void; 9 | onPointerLeave: () => void; 10 | onPointerDown: (event: import("mapbox-gl").MapMouseEvent) => void; 11 | onPointerMove: (event: import("mapbox-gl").MapMouseEvent) => void; 12 | onPointerUp: () => void; 13 | destroy(): void; 14 | } 15 | -------------------------------------------------------------------------------- /packages/image/types/modes/rotate.d.ts: -------------------------------------------------------------------------------- 1 | export class Rotate { 2 | constructor(map: import("mapbox-gl").Map, raster: import("../raster").Raster, onUpdate: (coordinates: import("../types").RasterCoordinates) => void); 3 | map: import("mapbox-gl").Map; 4 | raster: import("../raster").Raster; 5 | onUpdate: (coordinates: import("../types").RasterCoordinates) => void; 6 | centroid: [number, number] | null; 7 | startPoint: [number, number] | null; 8 | get id(): string; 9 | onPointerEnter: () => void; 10 | onPointerLeave: () => void; 11 | onPointerDown: (event: import("mapbox-gl").MapMouseEvent) => void; 12 | onPointerMove: (event: import("mapbox-gl").MapMouseEvent) => void; 13 | onPointerUp: () => void; 14 | destroy(): void; 15 | } 16 | -------------------------------------------------------------------------------- /packages/image/types/modes/scale.d.ts: -------------------------------------------------------------------------------- 1 | export class Scale { 2 | constructor(map: import("mapbox-gl").Map, raster: import("../raster").Raster, onUpdate: (coordinates: import("../types").RasterCoordinates) => void); 3 | map: import("mapbox-gl").Map; 4 | raster: import("../raster").Raster; 5 | onUpdate: (coordinates: import("../types").RasterCoordinates) => void; 6 | knobIndex: number | null; 7 | get id(): string; 8 | onPointerEnter: (event: import("mapbox-gl").MapMouseEvent) => void; 9 | onPointerLeave: () => void; 10 | onPointerDown: (event: import("mapbox-gl").MapMouseEvent) => void; 11 | onPointerMove: (event: import("mapbox-gl").MapMouseEvent) => void; 12 | onPointerUp: () => void; 13 | destroy(): void; 14 | } 15 | -------------------------------------------------------------------------------- /packages/image/types/raster.d.ts: -------------------------------------------------------------------------------- 1 | export class Raster { 2 | constructor(image: HTMLImageElement, coordinates: import("./types").RasterCoordinates); 3 | src: string; 4 | width: number; 5 | height: number; 6 | coordinates: import("./types").RasterCoordinates; 7 | locked: boolean; 8 | get id(): string; 9 | get rasterSource(): { 10 | id: string; 11 | source: import("mapbox-gl").ImageSourceSpecification; 12 | }; 13 | get polygonSource(): { 14 | id: string; 15 | source: { 16 | type: "geojson"; 17 | data: import("geojson").Feature; 18 | }; 19 | }; 20 | get pointsSource(): { 21 | id: string; 22 | source: { 23 | type: "geojson"; 24 | data: import("geojson").FeatureCollection; 25 | }; 26 | }; 27 | get rasterLayer(): import("mapbox-gl").RasterLayerSpecification; 28 | get fillLayer(): import("mapbox-gl").FillLayerSpecification; 29 | get contourLayer(): import("mapbox-gl").LineLayerSpecification; 30 | get knobsLayer(): import("mapbox-gl").CircleLayerSpecification; 31 | } 32 | -------------------------------------------------------------------------------- /packages/image/types/types.d.ts: -------------------------------------------------------------------------------- 1 | export type ControlOptions = { 2 | removeButton?: boolean; 3 | }; 4 | export type RasterCoordinates = [[number, number], [number, number], [number, number], [number, number]]; 5 | -------------------------------------------------------------------------------- /packages/inspect/README.md: -------------------------------------------------------------------------------- 1 | [<< all controls](/README.md) 2 | 3 | # 🐞 @mapbox-controls/inspect 4 | 5 | ![](https://github.com/korywka/mapbox-controls/assets/988471/51eaee3e-1f4d-4e9a-b177-fd36f8c5ece1) 6 | 7 | Control to debug map style layers and sources. 8 | 9 | ⚠️ Inspect Control doesn't work with new MapBox Standard style, since the MapBox API [doesn't work](https://github.com/mapbox/mapbox-gl-js/issues/13160) as documentated. 10 | 11 | ``` 12 | npm i @mapbox-controls/inspect 13 | ``` 14 | 15 | ```js 16 | import InspectControl from '@mapbox-controls/inspect'; 17 | import '@mapbox-controls/inspect/src/index.css'; 18 | 19 | map.addControl(new InspectControl(), 'bottom-right'); 20 | ``` 21 | 22 | ## Options 23 | 24 | ```ts 25 | export type ControlOptions = { 26 | console?: boolean; 27 | }; 28 | ``` 29 | -------------------------------------------------------------------------------- /packages/inspect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mapbox-controls/inspect", 3 | "version": "3.0.0", 4 | "description": "Debug map style layers and sources", 5 | "type": "module", 6 | "main": "./src/index.js", 7 | "scripts": { 8 | "build": "tsc" 9 | }, 10 | "dependencies": { 11 | "@mapbox-controls/helpers": "workspace:*" 12 | }, 13 | "devDependencies": { 14 | "mapbox-gl": "catalog:" 15 | }, 16 | "peerDependencies": { 17 | "mapbox-gl": ">=1.0.0 <4.0.0" 18 | }, 19 | "types": "./types/index.d.ts", 20 | "files": [ 21 | "src", 22 | "types" 23 | ], 24 | "publishConfig": { 25 | "access": "public", 26 | "registry": "https://registry.npmjs.org/" 27 | }, 28 | "repository": "korywka/mapbox-gl-controls", 29 | "license": "MIT" 30 | } -------------------------------------------------------------------------------- /packages/inspect/src/icons.js: -------------------------------------------------------------------------------- 1 | import { parseSVG } from '@mapbox-controls/helpers'; 2 | 3 | function inspect() { 4 | return parseSVG(` 5 | 6 | 7 | 8 | 9 | `); 10 | } 11 | 12 | export const icons = { 13 | inspect, 14 | }; 15 | -------------------------------------------------------------------------------- /packages/inspect/src/index.css: -------------------------------------------------------------------------------- 1 | .mapbox-ctrl-inspect button { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | color: #333; 6 | } 7 | 8 | .mapbox-ctrl-inspect button.-active { 9 | color: #4264fb; 10 | } 11 | 12 | .mapbox-ctrl-inspect-popup { 13 | position: absolute; 14 | padding: 8px; 15 | border-radius: 4px; 16 | background: #fff; 17 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); 18 | font-family: sans-serif; 19 | white-space: nowrap; 20 | transform: translate(-50%, 5px); 21 | } 22 | 23 | .mapbox-ctrl-inspect-popup::before { 24 | content: ''; 25 | width: 0; 26 | height: 0; 27 | position: absolute; 28 | bottom: 100%; 29 | left: 50%; 30 | transform: translate(-50%, 0); 31 | border-left: 5px solid transparent; 32 | border-right: 5px solid transparent; 33 | border-bottom: 5px solid #fff; 34 | } 35 | 36 | .mapbox-ctrl-inspect-popup header { 37 | display: flex; 38 | justify-content: space-between; 39 | align-items: center; 40 | font-size: 12px; 41 | } 42 | 43 | .mapbox-ctrl-inspect-popup nav { 44 | margin: 0 auto; 45 | } 46 | 47 | .mapbox-ctrl-inspect-popup button { 48 | flex: none; 49 | padding: 4px 12px; 50 | border-radius: 4px; 51 | border: none; 52 | background: none; 53 | font-family: sans-serif; 54 | font-size: 16px; 55 | color: #4264fb; 56 | cursor: pointer; 57 | } 58 | 59 | .mapbox-ctrl-inspect-popup table { 60 | width: 100%; 61 | min-width: 200px; 62 | max-width: 400px; 63 | border-collapse: collapse; 64 | } 65 | 66 | .mapbox-ctrl-inspect-popup tr:not(:last-child) td, 67 | .mapbox-ctrl-inspect-popup tr:not(:last-child) th { 68 | border-bottom: 1px solid rgba(0, 0, 0, 0.05); 69 | } 70 | 71 | .mapbox-ctrl-inspect-popup th, 72 | .mapbox-ctrl-inspect-popup td { 73 | width: 50%; 74 | padding: 5px; 75 | white-space: break-spaces; 76 | } 77 | 78 | .mapbox-ctrl-inspect-popup th { 79 | text-align: right; 80 | font-weight: 600; 81 | } 82 | 83 | .mapbox-ctrl-inspect-popup td[colspan="2"] { 84 | text-align: center; 85 | color: #4264fb; 86 | font-weight: 600; 87 | } -------------------------------------------------------------------------------- /packages/inspect/src/index.js: -------------------------------------------------------------------------------- 1 | import { controlButton, controlContainer } from '@mapbox-controls/helpers'; 2 | import { icons } from './icons.js'; 3 | import { popup } from './popup.js'; 4 | 5 | export default class InspectControl { 6 | /** @param {import('./types').ControlOptions} options */ 7 | constructor(options = {}) { 8 | this.options = { ...options }; 9 | this.container = controlContainer('mapbox-ctrl-inspect'); 10 | this.button = controlButton({ 11 | title: 'Inspect', 12 | icon: icons.inspect(), 13 | onClick: () => this.onControlButtonClick(), 14 | }); 15 | this.isActive = false; 16 | } 17 | 18 | onControlButtonClick() { 19 | if (this.isActive) { 20 | this.deactivate(); 21 | } else { 22 | this.activate(); 23 | } 24 | } 25 | 26 | activate() { 27 | if (!this.map) throw Error('map is undefined'); 28 | this.isActive = true; 29 | this.button.classList.add('-active'); 30 | this.map.on('click', this.mapClickListener); 31 | this.map.on('move', this.updatePosition); 32 | this.map.getCanvas().style.cursor = 'pointer'; 33 | } 34 | 35 | deactivate() { 36 | if (!this.map) throw Error('map is undefined'); 37 | this.isActive = false; 38 | this.button.classList.remove('-active'); 39 | this.map.off('click', this.mapClickListener); 40 | this.map.off('move', this.updatePosition); 41 | this.map.getCanvas().style.cursor = ''; 42 | this.hideDetails(); 43 | } 44 | 45 | /** @param {import('mapbox-gl').Point} point */ 46 | getPointFeatures(point) { 47 | if (!this.map) throw Error('map is undefined'); 48 | const selectThreshold = 3; 49 | 50 | /** @type {[[number, number], [number, number]]} */ 51 | const queryBox = [ 52 | [point.x - selectThreshold, point.y + selectThreshold], // bottom left (SW) 53 | [point.x + selectThreshold, point.y - selectThreshold], // top right (NE) 54 | ]; 55 | 56 | return this.map.queryRenderedFeatures(queryBox); 57 | } 58 | 59 | /** @param {import('mapbox-gl').GeoJSONFeature[]} features */ 60 | showDetails(features) { 61 | if (!this.map) throw Error('map is undefined'); 62 | this.detailsNode = popup(features); 63 | this.map.getContainer().appendChild(this.detailsNode); 64 | this.updatePosition(); 65 | if (this.options.console) { 66 | console.log(features); 67 | } 68 | } 69 | 70 | hideDetails() { 71 | if (!this.map) throw Error('map is undefined'); 72 | if (!this.detailsNode) return; 73 | this.map.getContainer().removeChild(this.detailsNode); 74 | this.detailsNode = undefined; 75 | } 76 | 77 | updatePosition = () => { 78 | if (!this.map) throw Error('map is undefined'); 79 | if (!this.lngLat) return; 80 | if (!this.detailsNode) return; 81 | const canvasRect = this.map.getCanvas().getBoundingClientRect(); 82 | const pos = this.map.project(this.lngLat); 83 | this.detailsNode.style.left = `${pos.x - canvasRect.left}px`; 84 | this.detailsNode.style.top = `${pos.y - canvasRect.top}px`; 85 | }; 86 | 87 | /** @param {import('mapbox-gl').MapMouseEvent} event */ 88 | mapClickListener = (event) => { 89 | this.lngLat = event.lngLat; 90 | const features = this.getPointFeatures(event.point); 91 | this.hideDetails(); 92 | this.showDetails(features); 93 | }; 94 | 95 | /** 96 | * @param {import('mapbox-gl').Map} map 97 | * @returns {HTMLElement} 98 | */ 99 | onAdd(map) { 100 | this.map = map; 101 | this.container.appendChild(this.button); 102 | return this.container; 103 | } 104 | 105 | onRemove() { 106 | this.deactivate(); 107 | this.container.parentNode?.removeChild(this.container); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /packages/inspect/src/popup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {import('mapbox-gl').GeoJSONFeature[]} features 3 | * @param {number} current 4 | * @returns {string} 5 | */ 6 | function html(features, current) { 7 | const feature = features[current]; 8 | const withProperties = feature.properties && Object.keys(feature.properties).length; 9 | const properties = feature.properties || {}; 10 | const layer = /** @type {import('mapbox-gl').LayerSpecification} */ (feature.layer); 11 | 12 | return (` 13 |
14 | ${features.length > 1 ? '' : ''} 15 | 18 | ${features.length > 1 ? '' : ''} 19 |
20 | 21 | ${feature.id ? (` 22 | 23 | 24 | 25 | 26 | `) : ''} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ${withProperties ? (` 47 | 48 | 49 | 50 | `) : ''} 51 | ${withProperties ? Object.entries(properties).map(([key, value]) => (` 52 | 53 | 54 | 55 | 56 | `)).join('') : ''} 57 |
$id${feature.id}
layer
id${layer.id}
type${layer.type}
source${layer.source}
source-layer${layer['source-layer'] ?? '-'}
properties
${key}${value}
58 | `); 59 | } 60 | 61 | /** 62 | * @param {import('mapbox-gl').GeoJSONFeature[]} features 63 | * @returns {HTMLDivElement} 64 | */ 65 | export function popup(features) { 66 | const node = document.createElement('div'); 67 | let current = 0; 68 | node.classList.add('mapbox-ctrl-inspect-popup'); 69 | 70 | if (!features.length) { 71 | node.textContent = 'No features'; 72 | return node; 73 | } 74 | 75 | node.innerHTML = html(features, current); 76 | 77 | node.addEventListener('click', (event) => { 78 | const target = /** @type {HTMLElement} */(event.target); 79 | if (target.matches('[data-prev]')) { 80 | const isFirst = current === 0; 81 | current = isFirst ? features.length - 1 : current - 1; 82 | } else if (target.matches('[data-next]')) { 83 | const isLast = current === features.length - 1; 84 | current = isLast ? 0 : current + 1; 85 | } 86 | node.innerHTML = ''; 87 | node.innerHTML = html(features, current); 88 | }); 89 | 90 | return node; 91 | } 92 | -------------------------------------------------------------------------------- /packages/inspect/src/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {{ 3 | * console?: boolean 4 | * }} ControlOptions 5 | */ 6 | 7 | export {}; 8 | -------------------------------------------------------------------------------- /packages/inspect/tsconfig.json: -------------------------------------------------------------------------------- 1 | ../../tsconfig.shared.json -------------------------------------------------------------------------------- /packages/inspect/types/icons.d.ts: -------------------------------------------------------------------------------- 1 | export namespace icons { 2 | export { inspect }; 3 | } 4 | declare function inspect(): SVGElement; 5 | export {}; 6 | -------------------------------------------------------------------------------- /packages/inspect/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export default class InspectControl { 2 | constructor(options?: import("./types").ControlOptions); 3 | options: { 4 | console?: boolean; 5 | }; 6 | container: HTMLDivElement; 7 | button: HTMLButtonElement; 8 | isActive: boolean; 9 | onControlButtonClick(): void; 10 | activate(): void; 11 | deactivate(): void; 12 | getPointFeatures(point: import("mapbox-gl").Point): import("mapbox-gl").GeoJSONFeature[]; 13 | showDetails(features: import("mapbox-gl").GeoJSONFeature[]): void; 14 | detailsNode: HTMLDivElement | undefined; 15 | hideDetails(): void; 16 | updatePosition: () => void; 17 | mapClickListener: (event: import("mapbox-gl").MapMouseEvent) => void; 18 | lngLat: import("mapbox-gl").LngLat | undefined; 19 | onAdd(map: import("mapbox-gl").Map): HTMLElement; 20 | map: import("mapbox-gl").Map | undefined; 21 | onRemove(): void; 22 | } 23 | -------------------------------------------------------------------------------- /packages/inspect/types/popup.d.ts: -------------------------------------------------------------------------------- 1 | export function popup(features: import("mapbox-gl").GeoJSONFeature[]): HTMLDivElement; 2 | -------------------------------------------------------------------------------- /packages/inspect/types/types.d.ts: -------------------------------------------------------------------------------- 1 | export type ControlOptions = { 2 | console?: boolean; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/language/README.md: -------------------------------------------------------------------------------- 1 | [<< all controls](/README.md) 2 | 3 | # 📖 @mapbox-controls/language 4 | 5 | ![](https://github.com/korywka/mapbox-controls/assets/988471/b2984a79-73f0-43e2-96b3-782b4b9970dc) 6 | 7 | Localize map or change dynamically language. 8 | 9 | By default, supported languages option is [the supported list](https://docs.mapbox.com/data/tilesets/reference/mapbox-streets-v8/#common-fields) by mapbox styles. 10 | 11 | ``` 12 | npm i @mapbox-controls/language 13 | ``` 14 | 15 | ```js 16 | // set language from browser, use 'mul' as fallback 17 | map.addControl(new LanguageControl()); 18 | 19 | // set custom language while initialization 20 | const languageControl = new LanguageControl({ 21 | language: 'ru', 22 | }); 23 | map.addControl(languageControl); 24 | 25 | // or change language dynamically 26 | languageControl.setLanguage(event.target.value); 27 | ``` 28 | 29 | ## Options 30 | 31 | ```ts 32 | export type ControlOptions = { 33 | supportedLanguages?: string[]; 34 | language?: string; 35 | getLanguageKey?: (language: string) => string; 36 | excludedLayerIds?: string[]; 37 | }; 38 | ``` 39 | 40 | ## Methods 41 | 42 | - `setLanguage(lang?: string | undefined): void;` - set dynamically map language 43 | -------------------------------------------------------------------------------- /packages/language/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mapbox-controls/language", 3 | "version": "3.0.0", 4 | "description": "Change map language", 5 | "type": "module", 6 | "main": "./src/index.js", 7 | "scripts": { 8 | "build": "tsc" 9 | }, 10 | "devDependencies": { 11 | "mapbox-gl": "catalog:" 12 | }, 13 | "peerDependencies": { 14 | "mapbox-gl": ">=1.0.0 <4.0.0" 15 | }, 16 | "types": "./types/index.d.ts", 17 | "files": [ 18 | "src", 19 | "types" 20 | ], 21 | "publishConfig": { 22 | "access": "public", 23 | "registry": "https://registry.npmjs.org/" 24 | }, 25 | "repository": "korywka/mapbox-controls", 26 | "license": "MIT" 27 | } -------------------------------------------------------------------------------- /packages/language/src/index.js: -------------------------------------------------------------------------------- 1 | const defaults = { 2 | supportedLanguages: ['ar', 'de', 'en', 'es', 'fr', 'it', 'ja', 'ko', 'mul', 'pt', 'ru', 'vi', 'zh-Hans', 'zh-Hant'], 3 | getLanguageKey: (/** @type {string} */ language) => (language === 'mul' ? 'name' : `name_${language}`), 4 | excludedLayerIds: [], 5 | }; 6 | 7 | export default class LanguageControl { 8 | /** @param {import('./types').ControlOptions} options */ 9 | constructor(options = {}) { 10 | this.options = { ...defaults, ...options }; 11 | this.container = document.createElement('div'); 12 | } 13 | 14 | styleChangeListener = () => { 15 | if (!this.map) throw Error('map is undefined'); 16 | this.map.off('styledata', this.styleChangeListener); 17 | this.setLanguage(this.options.language); 18 | }; 19 | 20 | /** @param {string=} lang */ 21 | setLanguage(lang) { 22 | if (!this.map) throw Error('map is undefined'); 23 | let language = lang || this.browserLanguage(); 24 | if (this.options.supportedLanguages.indexOf(language) < 0) { 25 | language = 'mul'; 26 | } 27 | const style = this.map.getStyle(); 28 | if (!style) return; 29 | const languageKey = this.options.getLanguageKey(language); 30 | const layers = style.layers.map((layer) => { 31 | if (layer.type !== 'symbol') return layer; 32 | if (!layer.layout || !layer.layout['text-field']) return layer; 33 | if (this.options.excludedLayerIds.indexOf(layer.id) !== -1) return layer; 34 | 35 | const textField = layer.layout['text-field']; 36 | const textFieldLocalized = this.localizeTextField(textField, languageKey); 37 | 38 | return { 39 | ...layer, 40 | layout: { 41 | ...layer.layout, 42 | 'text-field': textFieldLocalized, 43 | }, 44 | }; 45 | }); 46 | 47 | this.map.setStyle({ ...style, layers }); 48 | } 49 | 50 | browserLanguage() { 51 | const language = navigator?.languages[0] ?? navigator.language; 52 | const parts = language.split('-'); 53 | const languageCode = parts.length > 1 ? parts[0] : language; 54 | if (this.options.supportedLanguages.indexOf(languageCode) > -1) return languageCode; 55 | 56 | return 'mul'; 57 | } 58 | 59 | /** 60 | * @param {import('./types').TextField} field 61 | * @param {string} languageKey 62 | * @returns {import('./types').TextField} 63 | */ 64 | localizeTextField(field, languageKey) { 65 | // string 66 | if (typeof field === 'string') { 67 | return field.replace(/{name.*?}/, `{${languageKey}}`); 68 | } 69 | 70 | const str = JSON.stringify(field); 71 | 72 | // expression 73 | if (Array.isArray(field)) { 74 | return JSON.parse(str.replace( 75 | /"coalesce",\["get","name.*?"]/g, 76 | `"coalesce",["get","${languageKey}"]`, 77 | )); 78 | } 79 | 80 | // style function 81 | return JSON.parse(str.replace(/{name.*?}/g, `{${languageKey}}`)); 82 | } 83 | 84 | /** 85 | * @param {import('mapbox-gl').Map} map 86 | * @returns {HTMLElement} 87 | */ 88 | onAdd(map) { 89 | this.map = map; 90 | this.map.on('styledata', this.styleChangeListener); 91 | return this.container; 92 | } 93 | 94 | onRemove() { 95 | this.map?.off('styledata', this.styleChangeListener); 96 | this.container.parentNode?.removeChild(this.container); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/language/src/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {{ 3 | * supportedLanguages?: string[] 4 | * language?: string 5 | * getLanguageKey?: (language: string) => string 6 | * excludedLayerIds?: string[] 7 | * }} ControlOptions 8 | */ 9 | 10 | /** @typedef {import('mapbox-gl').DataDrivenPropertyValueSpecification} FunctionField */ 11 | /** @typedef {import('mapbox-gl').ExpressionSpecification} ExpressionFiled */ 12 | 13 | /** @typedef {string | FunctionField | ExpressionFiled} TextField */ 14 | 15 | export {}; 16 | -------------------------------------------------------------------------------- /packages/language/tsconfig.json: -------------------------------------------------------------------------------- 1 | ../../tsconfig.shared.json -------------------------------------------------------------------------------- /packages/language/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export default class LanguageControl { 2 | constructor(options?: import("./types").ControlOptions); 3 | options: { 4 | supportedLanguages: string[]; 5 | language?: string; 6 | getLanguageKey: (language: string) => string; 7 | excludedLayerIds: string[]; 8 | }; 9 | container: HTMLDivElement; 10 | styleChangeListener: () => void; 11 | setLanguage(lang?: string | undefined): void; 12 | browserLanguage(): string; 13 | localizeTextField(field: import("./types").TextField, languageKey: string): import("./types").TextField; 14 | onAdd(map: import("mapbox-gl").Map): HTMLElement; 15 | map: import("mapbox-gl").Map | undefined; 16 | onRemove(): void; 17 | } 18 | -------------------------------------------------------------------------------- /packages/language/types/types.d.ts: -------------------------------------------------------------------------------- 1 | export type ControlOptions = { 2 | supportedLanguages?: string[]; 3 | language?: string; 4 | getLanguageKey?: (language: string) => string; 5 | excludedLayerIds?: string[]; 6 | }; 7 | export type FunctionField = import("mapbox-gl").DataDrivenPropertyValueSpecification; 8 | export type ExpressionFiled = import("mapbox-gl").ExpressionSpecification; 9 | export type TextField = string | FunctionField | ExpressionFiled; 10 | -------------------------------------------------------------------------------- /packages/ruler/README.md: -------------------------------------------------------------------------------- 1 | [<< all controls](/README.md) 2 | 3 | # 📏 @mapbox-controls/ruler 4 | 5 | ![](https://github.com/korywka/mapbox-controls/assets/988471/1c90555c-2e22-4785-8365-14c1bacabb18) 6 | 7 | Control to measure distance between points on a map. 8 | 9 | ``` 10 | npm i @mapbox-controls/ruler 11 | ``` 12 | 13 | ```js 14 | import RulerControl from '@mapbox-controls/ruler'; 15 | import '@mapbox-controls/ruler/src/index.css'; 16 | 17 | map.addControl(new RulerControl(), 'bottom-right'); 18 | map.on('ruler.on', () => console.log('Ruler activated')); 19 | map.on('ruler.off', () => console.log('Ruler deactivated')); 20 | ``` 21 | 22 | ## Options 23 | 24 | ```ts 25 | export type ControlOptions = { 26 | units?: import("@turf/helpers").Units; 27 | labelFormat?: (n: number) => string; 28 | lineLayout?: import("mapbox-gl").LineLayerSpecification["layout"]; 29 | linePaint?: import("mapbox-gl").LineLayerSpecification["paint"]; 30 | markerLayout?: import("mapbox-gl").CircleLayerSpecification["layout"]; 31 | markerPaint?: import("mapbox-gl").CircleLayerSpecification["paint"]; 32 | labelLayout?: import("mapbox-gl").SymbolLayerSpecification["layout"]; 33 | labelPaint?: import("mapbox-gl").SymbolLayerSpecification["paint"]; 34 | invisible?: boolean; 35 | }; 36 | ``` 37 | 38 | ## Events 39 | 40 | | event | description | 41 | | --------- | ----------------- | 42 | | ruler.on | ruler activated | 43 | | ruler.off | ruler deactivated | 44 | 45 | ## Methods 46 | 47 | Methods are useful for programmatic control (when option `invisible` is `true`): 48 | 49 | - `activate(): void;` - activate controls 50 | - `deactivate(): void;` - deactivate control 51 | - `addCoordinate(coordinate: [number, number]): void;` - add new coordinate 52 | -------------------------------------------------------------------------------- /packages/ruler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mapbox-controls/ruler", 3 | "version": "3.0.0", 4 | "description": "Measure distance between points on a map", 5 | "type": "module", 6 | "main": "./src/index.js", 7 | "scripts": { 8 | "build": "tsc" 9 | }, 10 | "dependencies": { 11 | "@mapbox-controls/helpers": "workspace:*", 12 | "@turf/distance": "7.1.0", 13 | "@turf/helpers": "7.1.0" 14 | }, 15 | "devDependencies": { 16 | "@types/geojson": "catalog:", 17 | "mapbox-gl": "catalog:" 18 | }, 19 | "peerDependencies": { 20 | "mapbox-gl": ">=1.0.0 <4.0.0" 21 | }, 22 | "types": "./types/index.d.ts", 23 | "files": [ 24 | "src", 25 | "types" 26 | ], 27 | "publishConfig": { 28 | "access": "public", 29 | "registry": "https://registry.npmjs.org/" 30 | }, 31 | "repository": "korywka/mapbox-gl-controls", 32 | "license": "MIT" 33 | } -------------------------------------------------------------------------------- /packages/ruler/src/icons.js: -------------------------------------------------------------------------------- 1 | import { parseSVG } from '@mapbox-controls/helpers'; 2 | 3 | function ruler() { 4 | return parseSVG(` 5 | 6 | 7 | 8 | 9 | `); 10 | } 11 | 12 | export const icons = { 13 | ruler, 14 | }; 15 | -------------------------------------------------------------------------------- /packages/ruler/src/index.css: -------------------------------------------------------------------------------- 1 | .mapbox-ctrl-ruler button { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | color: #333; 6 | } 7 | 8 | .mapbox-ctrl-ruler button.-active { 9 | color: #4264fb; 10 | } -------------------------------------------------------------------------------- /packages/ruler/src/index.js: -------------------------------------------------------------------------------- 1 | import { controlContainer, controlButton } from '@mapbox-controls/helpers'; 2 | import { icons } from './icons.js'; 3 | import { layers } from './layers.js'; 4 | import { sources, toGeoJSONLine, toGeoJSONPoints } from './sources.js'; 5 | 6 | export default class RulerControl { 7 | /** 8 | * @param {import('./types').ControlOptions} options 9 | */ 10 | constructor(options = {}) { 11 | this.options = options; 12 | this.container = controlContainer('mapbox-ctrl-ruler'); 13 | this.isActive = false; 14 | /** @type {[number, number][]} */ 15 | this.coordinates = []; 16 | /** @type {HTMLButtonElement | null} */ 17 | this.button = null; 18 | /** @type {(() => void) | null} */ 19 | this.removeDragEvents = null; 20 | if (!this.options.invisible) { 21 | this.button = controlButton({ 22 | title: 'Ruler', 23 | icon: icons.ruler(), 24 | onClick: () => this.onControlButtonClick(), 25 | }); 26 | } 27 | } 28 | 29 | onControlButtonClick() { 30 | if (this.isActive) { 31 | this.deactivate(); 32 | } else { 33 | this.activate(); 34 | } 35 | } 36 | 37 | draw = () => { 38 | if (!this.map) throw Error('map is undefined'); 39 | 40 | this.map.addSource(sources.line, { 41 | type: 'geojson', 42 | data: toGeoJSONLine(this.coordinates), 43 | }); 44 | 45 | this.map.addSource(sources.points, { 46 | type: 'geojson', 47 | data: toGeoJSONPoints(this.coordinates, { 48 | units: this.options.units, 49 | labelFormat: this.options.labelFormat, 50 | }), 51 | }); 52 | 53 | this.map.addLayer({ 54 | ...layers.line, 55 | layout: { 56 | ...layers.line.layout, 57 | ...this.options.lineLayout, 58 | }, 59 | paint: { 60 | ...layers.line.paint, 61 | ...this.options.linePaint, 62 | }, 63 | }); 64 | 65 | this.map.addLayer({ 66 | ...layers.markers, 67 | layout: { 68 | ...layers.markers.layout, 69 | ...this.options.markerLayout, 70 | }, 71 | paint: { 72 | ...layers.markers.paint, 73 | ...this.options.markerPaint, 74 | }, 75 | }); 76 | 77 | this.map.addLayer({ 78 | ...layers.labels, 79 | layout: { 80 | ...layers.labels.layout, 81 | ...this.options.labelLayout, 82 | }, 83 | paint: { 84 | ...layers.labels.paint, 85 | ...this.options.labelPaint, 86 | }, 87 | }); 88 | }; 89 | 90 | activate() { 91 | if (!this.map) throw Error('map is undefined'); 92 | const map = this.map; 93 | this.isActive = true; 94 | this.coordinates = []; 95 | map.getCanvas().style.cursor = 'crosshair'; 96 | this.draw(); 97 | map.on('click', this.mapClickListener); 98 | map.on('style.load', this.draw); 99 | // @ts-ignore 100 | map.fire('ruler.on'); 101 | if (this.button) { 102 | this.button.classList.add('-active'); 103 | } 104 | } 105 | 106 | deactivate() { 107 | if (!this.map) throw Error('map is undefined'); 108 | this.isActive = false; 109 | this.map.getCanvas().style.cursor = ''; 110 | // remove layers, sources and event listeners 111 | this.map.removeLayer(layers.line.id); 112 | this.map.removeLayer(layers.markers.id); 113 | this.map.removeLayer(layers.labels.id); 114 | this.map.removeSource(sources.line); 115 | this.map.removeSource(sources.points); 116 | this.map.off('click', this.mapClickListener); 117 | this.map.off('style.load', this.draw); 118 | // @ts-ignore 119 | this.map.fire('ruler.off'); 120 | if (this.button) { 121 | this.button.classList.remove('-active'); 122 | } 123 | } 124 | 125 | /** 126 | * @param {import('mapbox-gl').MapMouseEvent} event 127 | */ 128 | mapClickListener = (event) => { 129 | if (!this.map) throw Error('map is undefined'); 130 | this.addCoordinate([event.lngLat.lng, event.lngLat.lat]); 131 | }; 132 | 133 | /** 134 | * @param {[number, number]} coordinate - [lng, lat] of new point 135 | */ 136 | addCoordinate(coordinate) { 137 | if (!this.map) throw Error('map is undefined'); 138 | if (!this.isActive) throw Error('ruler is not active'); 139 | this.coordinates.push(coordinate); 140 | this.updateSource(); 141 | } 142 | 143 | updateSource() { 144 | if (!this.map) throw Error('map is undefined'); 145 | // @ts-ignore 146 | this.map.fire('ruler.change', { coordinates: this.coordinates }); 147 | const lineSource = /** @type {import('mapbox-gl').GeoJSONSource} */(this.map.getSource(sources.line)); 148 | const pointsSource = /** @type {import('mapbox-gl').GeoJSONSource} */(this.map.getSource(sources.points)); 149 | const geoJSONLine = toGeoJSONLine(this.coordinates); 150 | const geoJSONPoints = toGeoJSONPoints(this.coordinates, { 151 | units: this.options.units, 152 | labelFormat: this.options.labelFormat, 153 | }); 154 | lineSource.setData(geoJSONLine); 155 | pointsSource.setData(geoJSONPoints); 156 | } 157 | 158 | addDragEvents() { 159 | /** @typedef {import('mapbox-gl').MapMouseEvent} MapMouseEvent */ 160 | /** @typedef {import('mapbox-gl').MapTouchEvent} MapTouchEvent */ 161 | /** @typedef {import('mapbox-gl').MapMouseEvent} MapLayerMouseEvent */ 162 | /** @typedef {import('mapbox-gl').MapTouchEvent} MapLayerTouchEvent */ 163 | if (!this.map) throw Error('map is undefined'); 164 | const self = this; 165 | const map = this.map; 166 | const canvas = map.getCanvas(); 167 | /** @type {number} */ 168 | let markerIndex; 169 | 170 | function onMouseEnter() { 171 | canvas.style.cursor = 'move'; 172 | } 173 | 174 | function onMouseLeave() { 175 | canvas.style.cursor = ''; 176 | } 177 | 178 | /** @param {MapLayerMouseEvent | MapLayerTouchEvent} event */ 179 | function onStart(event) { 180 | // do not block multi-touch actions 181 | if (event.type === 'touchstart' && event.points.length !== 1) { 182 | return; 183 | } 184 | event.preventDefault(); 185 | const features = event.features; 186 | if (!features) return; 187 | markerIndex = Number(features[0].id); 188 | canvas.style.cursor = 'grabbing'; 189 | // mouse events 190 | map.on('mousemove', onMove); 191 | map.on('mouseup', onEnd); 192 | // touch events 193 | map.on('touchmove', onMove); 194 | map.on('touchend', onEnd); 195 | } 196 | 197 | /** @param {MapMouseEvent | MapTouchEvent} event */ 198 | function onMove(event) { 199 | const coords = event.lngLat; 200 | canvas.style.cursor = 'grabbing'; 201 | self.coordinates[markerIndex] = [coords.lng, coords.lat]; 202 | self.updateSource(); 203 | } 204 | 205 | function onEnd() { 206 | // mouse events 207 | map.off('mousemove', onMove); 208 | map.off('mouseup', onEnd); 209 | // touch events 210 | map.off('touchmove', onMove); 211 | map.off('touchend', onEnd); 212 | } 213 | 214 | // mouse events 215 | map.on('mouseenter', layers.markers.id, onMouseEnter); 216 | map.on('mouseleave', layers.markers.id, onMouseLeave); 217 | map.on('mousedown', layers.markers.id, onStart); 218 | // touch events 219 | map.on('touchstart', layers.markers.id, onStart); 220 | 221 | this.removeDragEvents = () => { 222 | // mouse events 223 | map.off('mousedown', layers.markers.id, onStart); 224 | map.off('mousemove', onMove); 225 | map.off('mouseup', onEnd); 226 | map.off('mouseenter', layers.markers.id, onMouseEnter); 227 | map.off('mouseleave', layers.markers.id, onMouseLeave); 228 | // touch events 229 | map.off('touchstart', layers.markers.id, onStart); 230 | map.off('touchmove', onMove); 231 | map.off('touchend', onEnd); 232 | }; 233 | } 234 | 235 | /** 236 | * @param {import('mapbox-gl').Map} map 237 | * @returns {HTMLElement} 238 | */ 239 | onAdd(map) { 240 | this.map = map; 241 | if (this.button) { 242 | this.container.appendChild(this.button); 243 | } 244 | this.addDragEvents(); 245 | return this.container; 246 | } 247 | 248 | onRemove() { 249 | if (this.isActive) { 250 | this.deactivate(); 251 | } 252 | if (this.removeDragEvents) { 253 | this.removeDragEvents(); 254 | } 255 | this.container.parentNode?.removeChild(this.container); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /packages/ruler/src/layers.js: -------------------------------------------------------------------------------- 1 | import { sources } from './sources.js'; 2 | 3 | /** 4 | * @typedef {{ 5 | * line: import('mapbox-gl').LineLayerSpecification 6 | * markers: import('mapbox-gl').CircleLayerSpecification 7 | * labels: import('mapbox-gl').SymbolLayerSpecification 8 | * }} Layers 9 | */ 10 | 11 | /** @type {Layers} */ 12 | export const layers = { 13 | line: { 14 | id: 'mapbox-control-ruler-line', 15 | type: 'line', 16 | source: sources.line, 17 | layout: {}, 18 | paint: { 19 | 'line-color': '#263238', 20 | 'line-width': 2, 21 | }, 22 | }, 23 | markers: { 24 | id: 'mapbox-control-ruler-markers', 25 | type: 'circle', 26 | source: sources.points, 27 | paint: { 28 | 'circle-radius': 5, 29 | 'circle-color': '#fff', 30 | 'circle-stroke-width': 2, 31 | 'circle-stroke-color': '#000', 32 | }, 33 | }, 34 | labels: { 35 | id: 'mapbox-control-ruler-labels', 36 | type: 'symbol', 37 | source: sources.points, 38 | layout: { 39 | 'text-field': '{distance}', 40 | 'text-font': ['Roboto Medium'], 41 | 'text-anchor': 'top', 42 | 'text-size': 12, 43 | 'text-offset': [0, 0.8], 44 | }, 45 | paint: { 46 | 'text-color': '#263238', 47 | 'text-halo-color': '#fff', 48 | 'text-halo-width': 1, 49 | }, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /packages/ruler/src/sources.js: -------------------------------------------------------------------------------- 1 | import distance from '@turf/distance'; 2 | 3 | /** @param {number} value */ 4 | function defaultLabelFormat(value) { 5 | return value < 1 6 | ? `${(value * 1000).toFixed()} m` 7 | : `${value.toFixed(2)} km`; 8 | } 9 | 10 | export const sources = { 11 | line: 'mapbox-control-ruler-lines', 12 | points: 'mapbox-control-ruler-points', 13 | }; 14 | 15 | /** 16 | * @param {[number, number][]} coordinates 17 | * @returns {import('geojson').Feature} 18 | */ 19 | export function toGeoJSONLine(coordinates) { 20 | return { 21 | type: 'Feature', 22 | properties: {}, 23 | geometry: { 24 | type: 'LineString', 25 | coordinates, 26 | }, 27 | }; 28 | } 29 | 30 | /** 31 | * @param {[number, number][]} coordinates 32 | * @param {{ 33 | * units?: import('@turf/helpers').Units, 34 | * labelFormat?: (v: number) => string 35 | * }} options 36 | * @returns {import('geojson').FeatureCollection} 37 | */ 38 | export function toGeoJSONPoints(coordinates, options = {}) { 39 | const labelFormat = options.labelFormat ?? defaultLabelFormat; 40 | const units = options.units ?? 'kilometers'; 41 | let sum = 0; 42 | return { 43 | type: 'FeatureCollection', 44 | features: coordinates.map((coordinate, index) => { 45 | if (index > 0) { 46 | sum += distance(coordinates[index - 1], coordinate, { units }); 47 | } 48 | return { 49 | type: 'Feature', 50 | id: String(index), 51 | properties: { 52 | distance: labelFormat(sum), 53 | }, 54 | geometry: { 55 | type: 'Point', 56 | coordinates: coordinate, 57 | }, 58 | }; 59 | }), 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /packages/ruler/src/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {{ 3 | * units?: import('@turf/helpers').Units 4 | * labelFormat?: (n: number) => string 5 | * lineLayout?: import('mapbox-gl').LineLayerSpecification['layout'] 6 | * linePaint?: import('mapbox-gl').LineLayerSpecification['paint'] 7 | * markerLayout?: import('mapbox-gl').CircleLayerSpecification['layout'] 8 | * markerPaint?: import('mapbox-gl').CircleLayerSpecification['paint'] 9 | * labelLayout?: import('mapbox-gl').SymbolLayerSpecification['layout'] 10 | * labelPaint?: import('mapbox-gl').SymbolLayerSpecification['paint'] 11 | * invisible?: boolean 12 | * }} ControlOptions 13 | */ 14 | 15 | export {}; 16 | -------------------------------------------------------------------------------- /packages/ruler/tsconfig.json: -------------------------------------------------------------------------------- 1 | ../../tsconfig.shared.json -------------------------------------------------------------------------------- /packages/ruler/types/distance.d.ts: -------------------------------------------------------------------------------- 1 | export function distance(from: [number, number], to: [number, number], units: import('@mapbox-controls/helpers').Units): number; 2 | -------------------------------------------------------------------------------- /packages/ruler/types/icons.d.ts: -------------------------------------------------------------------------------- 1 | export namespace icons { 2 | export { ruler }; 3 | } 4 | declare function ruler(): SVGElement; 5 | export {}; 6 | -------------------------------------------------------------------------------- /packages/ruler/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export default class RulerControl { 2 | constructor(options?: import("./types").ControlOptions); 3 | options: import("./types").ControlOptions; 4 | container: HTMLDivElement; 5 | isActive: boolean; 6 | coordinates: [number, number][]; 7 | button: HTMLButtonElement | null; 8 | removeDragEvents: (() => void) | null; 9 | onControlButtonClick(): void; 10 | draw: () => void; 11 | activate(): void; 12 | deactivate(): void; 13 | mapClickListener: (event: import("mapbox-gl").MapMouseEvent) => void; 14 | addCoordinate(coordinate: [number, number]): void; 15 | updateSource(): void; 16 | addDragEvents(): void; 17 | onAdd(map: import("mapbox-gl").Map): HTMLElement; 18 | map: import("mapbox-gl").Map | undefined; 19 | onRemove(): void; 20 | } 21 | -------------------------------------------------------------------------------- /packages/ruler/types/label-format.d.ts: -------------------------------------------------------------------------------- 1 | export function labelFormat(number: number): string; 2 | -------------------------------------------------------------------------------- /packages/ruler/types/layers.d.ts: -------------------------------------------------------------------------------- 1 | export const layers: Layers; 2 | export type Layers = { 3 | line: import("mapbox-gl").LineLayerSpecification; 4 | markers: import("mapbox-gl").CircleLayerSpecification; 5 | labels: import("mapbox-gl").SymbolLayerSpecification; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/ruler/types/sources.d.ts: -------------------------------------------------------------------------------- 1 | export function toGeoJSONLine(coordinates: [number, number][]): import("geojson").Feature; 2 | export function toGeoJSONPoints(coordinates: [number, number][], options?: { 3 | units?: import("@turf/helpers").Units; 4 | labelFormat?: (v: number) => string; 5 | }): import("geojson").FeatureCollection; 6 | export namespace sources { 7 | let line: string; 8 | let points: string; 9 | } 10 | -------------------------------------------------------------------------------- /packages/ruler/types/types.d.ts: -------------------------------------------------------------------------------- 1 | export type ControlOptions = { 2 | units?: import("@turf/helpers").Units; 3 | labelFormat?: (n: number) => string; 4 | lineLayout?: import("mapbox-gl").LineLayerSpecification["layout"]; 5 | linePaint?: import("mapbox-gl").LineLayerSpecification["paint"]; 6 | markerLayout?: import("mapbox-gl").CircleLayerSpecification["layout"]; 7 | markerPaint?: import("mapbox-gl").CircleLayerSpecification["paint"]; 8 | labelLayout?: import("mapbox-gl").SymbolLayerSpecification["layout"]; 9 | labelPaint?: import("mapbox-gl").SymbolLayerSpecification["paint"]; 10 | invisible?: boolean; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/styles/README.md: -------------------------------------------------------------------------------- 1 | [<< all controls](/README.md) 2 | 3 | # 💅 @mapbox-controls/styles 4 | 5 | ![](https://github.com/korywka/mapbox-controls/assets/988471/18ac929f-06ff-4043-8b4b-258b876b9585) 6 | 7 | Control to change map style among provided. 8 | 9 | ``` 10 | npm i @mapbox-controls/styles 11 | ``` 12 | 13 | ```js 14 | import StylesControl from '@mapbox-controls/styles'; 15 | import '@mapbox-controls/styles/src/index.css'; 16 | 17 | map.addControl(new StylesControl(styles: { 18 | label: 'Streets', 19 | styleName: 'Mapbox Streets', 20 | styleUrl: 'mapbox://styles/mapbox/streets-v12', 21 | }, { 22 | label: 'Satellite', 23 | styleName: 'Mapbox Satellite Streets', 24 | styleUrl: 'mapbox://sprites/mapbox/satellite-streets-v12', 25 | }), 'top-left'); 26 | 27 | // or with compact view and default styles (streets and satellite) 28 | map.addControl(new StylesControl({ compact: true }), 'top-left'); 29 | ``` 30 | 31 | Use mapbox [`style.load`](https://docs.mapbox.com/mapbox-gl-js/api/map/#map.event:style.load) event to redraw layers. 32 | 33 | ## Options 34 | 35 | `styleName` - is the root value of style's `name` property according to [the specification](https://docs.mapbox.com/style-spec/reference/root/#name). 36 | 37 | ```ts 38 | export type Style = { 39 | label: string; 40 | styleName: string; 41 | styleUrl: string; 42 | }; 43 | 44 | export type ControlOptions = { 45 | styles?: Style[]; 46 | onChange?: (style: Style) => void; 47 | compact?: boolean; 48 | }; 49 | ``` 50 | -------------------------------------------------------------------------------- /packages/styles/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mapbox-controls/styles", 3 | "version": "3.0.0", 4 | "description": "Change map style", 5 | "type": "module", 6 | "main": "./src/index.js", 7 | "scripts": { 8 | "build": "tsc" 9 | }, 10 | "dependencies": { 11 | "@mapbox-controls/helpers": "workspace:*" 12 | }, 13 | "devDependencies": { 14 | "mapbox-gl": "catalog:" 15 | }, 16 | "peerDependencies": { 17 | "mapbox-gl": ">=1.0.0 <4.0.0" 18 | }, 19 | "types": "./types/index.d.ts", 20 | "files": [ 21 | "src", 22 | "types" 23 | ], 24 | "publishConfig": { 25 | "access": "public", 26 | "registry": "https://registry.npmjs.org/" 27 | }, 28 | "repository": "korywka/mapbox-controls", 29 | "license": "MIT" 30 | } -------------------------------------------------------------------------------- /packages/styles/src/icons.js: -------------------------------------------------------------------------------- 1 | import { parseSVG } from '@mapbox-controls/helpers'; 2 | 3 | function layers() { 4 | return parseSVG(` 5 | 6 | 7 | 8 | `); 9 | } 10 | 11 | export const icons = { 12 | layers, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/styles/src/index.css: -------------------------------------------------------------------------------- 1 | .mapbox-ctrl-styles-expanded { 2 | display: flex; 3 | } 4 | 5 | .mapbox-ctrl-styles-expanded button { 6 | width: auto; 7 | padding: 0 8px; 8 | color: #333; 9 | } 10 | 11 | .mapboxgl-ctrl-group.mapbox-ctrl-styles-expanded button { 12 | border-radius: 0; 13 | } 14 | 15 | .mapboxgl-ctrl-group.mapbox-ctrl-styles-expanded button:first-child { 16 | border-radius: 4px 0 0 4px; 17 | } 18 | 19 | .mapboxgl-ctrl-group.mapbox-ctrl-styles-expanded button:last-child { 20 | border-radius: 0 4px 4px 0; 21 | } 22 | 23 | .mapbox-ctrl-styles-expanded button + button { 24 | border: none; 25 | } 26 | 27 | .mapbox-ctrl-styles-expanded button.-active { 28 | background: rgba(0, 0, 0, 0.05); 29 | } 30 | 31 | .mapbox-ctrl-styles-compact button { 32 | position: relative; 33 | display: flex; 34 | align-items: center; 35 | justify-content: center; 36 | color: #333; 37 | } 38 | 39 | .mapbox-ctrl-styles-compact select { 40 | position: absolute; 41 | left: 0; 42 | top: 0; 43 | right: 0; 44 | bottom: 0; 45 | opacity: 0; 46 | cursor: pointer; 47 | } 48 | -------------------------------------------------------------------------------- /packages/styles/src/index.js: -------------------------------------------------------------------------------- 1 | import { controlButton, controlContainer } from '@mapbox-controls/helpers'; 2 | import { icons } from './icons.js'; 3 | 4 | const defaults = [ 5 | { 6 | label: 'Standard', 7 | styleName: 'Mapbox Standard', 8 | styleUrl: 'mapbox://styles/mapbox/standard', 9 | }, { 10 | label: 'Satellite', 11 | styleName: 'Mapbox Satellite Streets', 12 | styleUrl: 'mapbox://styles/mapbox/satellite-streets-v12', 13 | }, 14 | ]; 15 | 16 | export default class StylesControl { 17 | /** @param {import('./types').ControlOptions} options */ 18 | constructor(options = {}) { 19 | this.options = { styles: defaults, ...options }; 20 | this.container = controlContainer('mapbox-ctrl-styles'); 21 | this.container.classList.add(options.compact ? 'mapbox-ctrl-styles-compact' : 'mapbox-ctrl-styles-expanded'); 22 | } 23 | 24 | /** @param {string} name */ 25 | findStyle(name) { 26 | const style = this.options.styles.find((s) => s.styleName === name); 27 | if (!style) throw Error(`can't find style with name ${name}`); 28 | return style; 29 | } 30 | 31 | getCurrentStyleName() { 32 | if (!this.map) throw Error('map is undefined'); 33 | /** @type {string} */ 34 | let name; 35 | /** @type {any} mapbox standard style doesn't return JSON Style object */ 36 | const style = this.map.getStyle(); 37 | if (Array.isArray(style.imports) && style.imports.length) { 38 | // mapbox standard style 39 | name = style.imports[0].data.name; 40 | } else { 41 | // classic style 42 | name = style.name; 43 | } 44 | if (!name) throw Error('style must have name'); 45 | return name; 46 | } 47 | 48 | expanded() { 49 | if (!this.map) throw Error('map is undefined'); 50 | /** @type HTMLButtonElement[] */ 51 | const buttons = []; 52 | this.options.styles.forEach((style) => { 53 | const button = controlButton({ 54 | title: style.label, 55 | textContent: style.label, 56 | onClick: () => { 57 | if (!this.map) throw Error('map is undefined'); 58 | if (button.classList.contains('-active')) return; 59 | this.map.setStyle(style.styleUrl); 60 | if (this.options.onChange) this.options.onChange(style); 61 | }, 62 | }); 63 | buttons.push(button); 64 | this.container.appendChild(button); 65 | }); 66 | 67 | this.map.on('styledata', () => { 68 | if (!this.map) throw Error('map is undefined'); 69 | buttons.forEach((button) => { 70 | button.classList.remove('-active'); 71 | }); 72 | const styleNames = this.options.styles.map((style) => style.styleName); 73 | const currentStyleName = this.getCurrentStyleName(); 74 | const currentStyleIndex = styleNames.indexOf(currentStyleName); 75 | if (currentStyleIndex !== -1) { 76 | const currentButton = buttons[currentStyleIndex]; 77 | currentButton.classList.add('-active'); 78 | } 79 | }); 80 | } 81 | 82 | compact() { 83 | if (!this.map) throw Error('map is undefined'); 84 | const button = controlButton({ title: 'Styles', icon: icons.layers() }); 85 | const select = document.createElement('select'); 86 | this.container.appendChild(button); 87 | button.appendChild(select); 88 | 89 | this.options.styles.forEach((style) => { 90 | const option = document.createElement('option'); 91 | select.appendChild(option); 92 | option.textContent = style.label; 93 | option.value = style.styleName; 94 | }); 95 | 96 | select.addEventListener('change', () => { 97 | if (!this.map) throw Error('map is undefined'); 98 | const style = this.findStyle(select.value); 99 | this.map.setStyle(style.styleUrl); 100 | if (this.options.onChange) this.options.onChange(style); 101 | }); 102 | 103 | this.map.on('styledata', () => { 104 | select.value = this.getCurrentStyleName(); 105 | }); 106 | } 107 | 108 | /** 109 | * @param {import('mapbox-gl').Map} map 110 | * @returns {HTMLElement} 111 | */ 112 | onAdd(map) { 113 | this.map = map; 114 | if (this.options.compact) { 115 | this.compact(); 116 | } else { 117 | this.expanded(); 118 | } 119 | return this.container; 120 | } 121 | 122 | onRemove() { 123 | this.container.parentNode?.removeChild(this.container); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /packages/styles/src/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {{ 3 | * label: string 4 | * styleName: string 5 | * styleUrl: string 6 | * }} Style 7 | */ 8 | 9 | /** 10 | * @typedef {{ 11 | * styles?: Style[] 12 | * onChange?: (style: Style) => void 13 | * compact?: boolean 14 | * }} ControlOptions 15 | */ 16 | 17 | export {}; 18 | -------------------------------------------------------------------------------- /packages/styles/tsconfig.json: -------------------------------------------------------------------------------- 1 | ../../tsconfig.shared.json -------------------------------------------------------------------------------- /packages/styles/types/icons.d.ts: -------------------------------------------------------------------------------- 1 | export namespace icons { 2 | export { layers }; 3 | } 4 | declare function layers(): SVGElement; 5 | export {}; 6 | -------------------------------------------------------------------------------- /packages/styles/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export default class StylesControl { 2 | constructor(options?: import("./types").ControlOptions); 3 | options: { 4 | styles: import("./types").Style[]; 5 | onChange?: (style: import("./types").Style) => void; 6 | compact?: boolean; 7 | }; 8 | container: HTMLDivElement; 9 | findStyle(name: string): import("./types").Style; 10 | getCurrentStyleName(): string; 11 | expanded(): void; 12 | compact(): void; 13 | onAdd(map: import("mapbox-gl").Map): HTMLElement; 14 | map: import("mapbox-gl").Map | undefined; 15 | onRemove(): void; 16 | } 17 | -------------------------------------------------------------------------------- /packages/styles/types/types.d.ts: -------------------------------------------------------------------------------- 1 | export type Style = { 2 | label: string; 3 | styleName: string; 4 | styleUrl: string; 5 | }; 6 | export type ControlOptions = { 7 | styles?: Style[]; 8 | onChange?: (style: Style) => void; 9 | compact?: boolean; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/tooltip/README.md: -------------------------------------------------------------------------------- 1 | [<< all controls](/README.md) 2 | 3 | # 🏷️ @mapbox-controls/tooltip 4 | 5 | ![](https://github.com/korywka/mapbox-controls/assets/988471/ac7b2ba3-352e-48e2-b1fa-79fc73c1b63c) 6 | 7 | Control to display tooltip on hover. The content of the tooltip may depend on the data stored in the layer. 8 | 9 | ``` 10 | npm i @mapbox-controls/tooltip 11 | ``` 12 | 13 | ```js 14 | import TooltipControl from '@mapbox-controls/tooltip'; 15 | import '@mapbox-controls/tooltip/src/index.css'; 16 | 17 | map.addControl(new TooltipControl({ 18 | getContent: (event) => `${event.lngLat.lng.toFixed(6)}, ${event.lngLat.lat.toFixed(6)}`, 19 | layer: 'some-layer-id', 20 | })); 21 | ``` 22 | 23 | ## Options 24 | 25 | ```ts 26 | export type ControlOptions = { 27 | getContent: (event: import("mapbox-gl").MapMouseEvent) => string; 28 | layer?: string; 29 | }; 30 | ``` 31 | -------------------------------------------------------------------------------- /packages/tooltip/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mapbox-controls/tooltip", 3 | "version": "3.0.0", 4 | "description": "Display tooltip on hover", 5 | "type": "module", 6 | "main": "./src/index.js", 7 | "scripts": { 8 | "build": "tsc" 9 | }, 10 | "devDependencies": { 11 | "mapbox-gl": "catalog:" 12 | }, 13 | "peerDependencies": { 14 | "mapbox-gl": ">=1.0.0 <4.0.0" 15 | }, 16 | "types": "./types/index.d.ts", 17 | "files": [ 18 | "src", 19 | "types" 20 | ], 21 | "publishConfig": { 22 | "access": "public", 23 | "registry": "https://registry.npmjs.org/" 24 | }, 25 | "repository": "korywka/mapbox-gl-controls", 26 | "license": "MIT" 27 | } -------------------------------------------------------------------------------- /packages/tooltip/src/index.css: -------------------------------------------------------------------------------- 1 | .mapbox-ctrl-tooltip { 2 | padding: 5px 7px; 3 | background: #fff; 4 | border-radius: 2px; 5 | position: absolute; 6 | transform: translate(-50%, 0); 7 | margin-top: 24px; 8 | font-size: 14px; 9 | white-space: nowrap; 10 | /* show tooltip over control buttons which z-index is 2 */ 11 | z-index: 3; 12 | } 13 | 14 | .mapbox-ctrl-tooltip:empty { 15 | display: none; 16 | } -------------------------------------------------------------------------------- /packages/tooltip/src/index.js: -------------------------------------------------------------------------------- 1 | class TooltipControl { 2 | /** @param {import('./types').ControlOptions} options */ 3 | constructor(options) { 4 | if (typeof options.getContent !== 'function') { 5 | throw Error('getContent function must be defined'); 6 | } 7 | this.options = { ...options }; 8 | this.container = document.createElement('div'); 9 | /** @type {import('mapbox-gl').MapEventType} */ 10 | this.eventShow = this.options.layer ? 'mouseenter' : 'mouseover'; 11 | /** @type {import('mapbox-gl').MapEventType} */ 12 | this.eventHide = this.options.layer ? 'mouseleave' : 'mouseout'; 13 | this.node = document.createElement('div'); 14 | this.node.classList.add('mapbox-ctrl-tooltip'); 15 | this.lngLat = undefined; 16 | this.cursorStyle = ''; 17 | } 18 | 19 | show = () => { 20 | if (!this.map) throw Error('map is undefined'); 21 | this.map.getContainer().appendChild(this.node); 22 | this.cursorStyle = this.map.getCanvas().style.cursor; 23 | this.map.getCanvas().style.cursor = 'pointer'; 24 | this.map.on('move', this.updatePosition); 25 | }; 26 | 27 | hide = () => { 28 | if (!this.map) throw Error('map is undefined'); 29 | this.node.innerHTML = ''; 30 | this.map.getContainer().removeChild(this.node); 31 | this.map.getCanvas().style.cursor = this.cursorStyle; 32 | this.map.off('move', this.updatePosition); 33 | }; 34 | 35 | /** @param {import('mapbox-gl').MapMouseEvent} event */ 36 | move = (event) => { 37 | this.node.innerHTML = this.options.getContent(event); 38 | this.lngLat = event.lngLat; 39 | this.updatePosition(); 40 | }; 41 | 42 | updatePosition = () => { 43 | if (!this.lngLat) return; 44 | if (!this.map) throw Error('map is undefined'); 45 | const pos = this.map.project(this.lngLat); 46 | this.node.style.left = `${pos.x}px`; 47 | this.node.style.top = `${pos.y}px`; 48 | }; 49 | 50 | /** 51 | * @param {import('mapbox-gl').Map} map 52 | * @returns {HTMLElement} 53 | */ 54 | onAdd(map) { 55 | this.map = map; 56 | if (this.options.layer) { 57 | this.map.on(this.eventShow, this.options.layer, this.show); 58 | this.map.on('mousemove', this.options.layer, this.move); 59 | this.map.on(this.eventHide, this.options.layer, this.hide); 60 | } else { 61 | this.map.on(this.eventShow, this.show); 62 | this.map.on('mousemove', this.move); 63 | this.map.on(this.eventHide, this.hide); 64 | } 65 | 66 | return this.container; 67 | } 68 | 69 | onRemove() { 70 | if (!this.map) throw Error('map is undefined'); 71 | if (this.options.layer) { 72 | this.map.off(this.eventShow, this.options.layer, this.show); 73 | this.map.off('mousemove', this.options.layer, this.move); 74 | this.map.off(this.eventHide, this.options.layer, this.hide); 75 | } else { 76 | this.map.off(this.eventShow, this.show); 77 | this.map.off('mousemove', this.move); 78 | this.map.off(this.eventHide, this.hide); 79 | } 80 | this.hide(); 81 | this.container.parentNode?.removeChild(this.container); 82 | } 83 | } 84 | 85 | export default TooltipControl; 86 | -------------------------------------------------------------------------------- /packages/tooltip/src/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {{ 3 | * getContent: (event: import('mapbox-gl').MapMouseEvent) => string 4 | * layer?: string 5 | * }} ControlOptions 6 | */ 7 | 8 | export {}; 9 | -------------------------------------------------------------------------------- /packages/tooltip/tsconfig.json: -------------------------------------------------------------------------------- 1 | ../../tsconfig.shared.json -------------------------------------------------------------------------------- /packages/tooltip/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export default TooltipControl; 2 | declare class TooltipControl { 3 | constructor(options: import("./types").ControlOptions); 4 | options: { 5 | getContent: (event: import("mapbox-gl").MapMouseEvent) => string; 6 | layer?: string; 7 | }; 8 | container: HTMLDivElement; 9 | eventShow: import("mapbox-gl").MapEventType; 10 | eventHide: import("mapbox-gl").MapEventType; 11 | node: HTMLDivElement; 12 | lngLat: import("mapbox-gl").LngLat | undefined; 13 | cursorStyle: string; 14 | show: () => void; 15 | hide: () => void; 16 | move: (event: import("mapbox-gl").MapMouseEvent) => void; 17 | updatePosition: () => void; 18 | onAdd(map: import("mapbox-gl").Map): HTMLElement; 19 | map: import("mapbox-gl").Map | undefined; 20 | onRemove(): void; 21 | } 22 | -------------------------------------------------------------------------------- /packages/tooltip/types/types.d.ts: -------------------------------------------------------------------------------- 1 | export type ControlOptions = { 2 | getContent: (event: import("mapbox-gl").MapMouseEvent) => string; 3 | layer?: string; 4 | }; 5 | -------------------------------------------------------------------------------- /packages/zoom/README.md: -------------------------------------------------------------------------------- 1 | [<< all controls](/README.md) 2 | 3 | # 🔍 @mapbox-controls/zoom 4 | 5 | ![](https://github.com/korywka/mapbox-controls/assets/988471/6f02cf18-765c-47c0-821c-5cb741d34972) 6 | 7 | Control to zoom in and zoom out map. The difference between the standard ones is that they are not combined with a compass control. 8 | 9 | ``` 10 | npm i @mapbox-controls/zoom 11 | ``` 12 | 13 | ```js 14 | import ZoomControl from '@mapbox-controls/zoom'; 15 | import '@mapbox-controls/zoom/src/index.css'; 16 | 17 | map.addControl(new ZoomControl(), 'bottom-right'); 18 | ``` 19 | -------------------------------------------------------------------------------- /packages/zoom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mapbox-controls/zoom", 3 | "version": "3.0.0", 4 | "description": "Zoom in and zoom out map", 5 | "type": "module", 6 | "main": "./src/index.js", 7 | "scripts": { 8 | "build": "tsc" 9 | }, 10 | "dependencies": { 11 | "@mapbox-controls/helpers": "workspace:*" 12 | }, 13 | "devDependencies": { 14 | "mapbox-gl": "catalog:" 15 | }, 16 | "peerDependencies": { 17 | "mapbox-gl": ">=1.0.0 <4.0.0" 18 | }, 19 | "types": "./types/index.d.ts", 20 | "files": [ 21 | "src", 22 | "types" 23 | ], 24 | "publishConfig": { 25 | "access": "public", 26 | "registry": "https://registry.npmjs.org/" 27 | }, 28 | "repository": "korywka/mapbox-controls", 29 | "license": "MIT" 30 | } -------------------------------------------------------------------------------- /packages/zoom/src/icons.js: -------------------------------------------------------------------------------- 1 | import { parseSVG } from '@mapbox-controls/helpers'; 2 | 3 | function plus() { 4 | return parseSVG(` 5 | 6 | 7 | 8 | 9 | `); 10 | } 11 | 12 | function minus() { 13 | return parseSVG(` 14 | 15 | 16 | 17 | 18 | `); 19 | } 20 | 21 | export const icons = { 22 | plus, 23 | minus, 24 | }; 25 | -------------------------------------------------------------------------------- /packages/zoom/src/index.css: -------------------------------------------------------------------------------- 1 | .mapbox-ctrl-zoom button { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | color: #333; 6 | } -------------------------------------------------------------------------------- /packages/zoom/src/index.js: -------------------------------------------------------------------------------- 1 | import { controlButton, controlContainer } from '@mapbox-controls/helpers'; 2 | import { icons } from './icons.js'; 3 | 4 | class ZoomControl { 5 | constructor() { 6 | this.container = controlContainer('mapbox-ctrl-zoom'); 7 | this.buttonIn = controlButton({ 8 | title: 'Zoom In', 9 | icon: icons.plus(), 10 | onClick: () => this.map?.zoomIn(), 11 | }); 12 | this.buttonOut = controlButton({ 13 | title: 'Zoom Out', 14 | icon: icons.minus(), 15 | onClick: () => this.map?.zoomOut(), 16 | }); 17 | } 18 | 19 | /** 20 | * @param {import('mapbox-gl').Map} map 21 | * @returns {HTMLElement} 22 | */ 23 | onAdd(map) { 24 | this.map = map; 25 | this.container.appendChild(this.buttonIn); 26 | this.container.appendChild(this.buttonOut); 27 | return this.container; 28 | } 29 | 30 | onRemove() { 31 | this.container.parentNode?.removeChild(this.container); 32 | } 33 | } 34 | 35 | export default ZoomControl; 36 | -------------------------------------------------------------------------------- /packages/zoom/tsconfig.json: -------------------------------------------------------------------------------- 1 | ../../tsconfig.shared.json -------------------------------------------------------------------------------- /packages/zoom/types/icons.d.ts: -------------------------------------------------------------------------------- 1 | export namespace icons { 2 | export { plus }; 3 | export { minus }; 4 | } 5 | declare function plus(): SVGElement; 6 | declare function minus(): SVGElement; 7 | export {}; 8 | -------------------------------------------------------------------------------- /packages/zoom/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export default ZoomControl; 2 | declare class ZoomControl { 3 | container: HTMLDivElement; 4 | buttonIn: HTMLButtonElement; 5 | buttonOut: HTMLButtonElement; 6 | onAdd(map: import("mapbox-gl").Map): HTMLElement; 7 | map: import("mapbox-gl").Map | undefined; 8 | onRemove(): void; 9 | } 10 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "preview" 4 | 5 | catalog: 6 | "mapbox-gl": 3.6.0 7 | "@types/geojson": 7946.0.14 -------------------------------------------------------------------------------- /preview/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MapBox Controls 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 25 | 26 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /preview/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preview", 3 | "private": "true", 4 | "type": "module", 5 | "scripts": { 6 | "start": "concurrently \"npm:serve\" \"npm:watch\"", 7 | "serve": "serve .", 8 | "build": "rollup -c", 9 | "watch": "rollup -c -w" 10 | }, 11 | "dependencies": { 12 | "@mapbox-controls/compass": "workspace:*", 13 | "@mapbox-controls/image": "workspace:*", 14 | "@mapbox-controls/inspect": "workspace:*", 15 | "@mapbox-controls/language": "workspace:*", 16 | "@mapbox-controls/ruler": "workspace:*", 17 | "@mapbox-controls/styles": "workspace:*", 18 | "@mapbox-controls/tooltip": "workspace:*", 19 | "@mapbox-controls/zoom": "workspace:*", 20 | "mapbox-gl": "catalog:" 21 | }, 22 | "devDependencies": { 23 | "@rollup/plugin-commonjs": "26.0.1", 24 | "@rollup/plugin-node-resolve": "15.2.3", 25 | "concurrently": "8.2.2", 26 | "rollup": "4.21.1", 27 | "rollup-plugin-import-css": "3.5.1", 28 | "rollup-plugin-polyfill-node": "0.13.0", 29 | "serve": "14.2.3" 30 | } 31 | } -------------------------------------------------------------------------------- /preview/plan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korywka/mapbox-controls/5945346540622c7bb69248663fc4fb9e3fafeb23/preview/plan.jpg -------------------------------------------------------------------------------- /preview/preview.bundle.css: -------------------------------------------------------------------------------- 1 | .mapboxgl-map{font:12px/20px Helvetica Neue,Arial,Helvetica,sans-serif;overflow:hidden;position:relative;-webkit-tap-highlight-color:rgb(0 0 0/0)}.mapboxgl-canvas{left:0;position:absolute;top:0}.mapboxgl-map:-webkit-full-screen{height:100%;width:100%}.mapboxgl-canary{background-color:salmon}.mapboxgl-canvas-container.mapboxgl-interactive,.mapboxgl-ctrl-group button.mapboxgl-ctrl-compass{cursor:grab;-webkit-user-select:none;user-select:none}.mapboxgl-canvas-container.mapboxgl-interactive.mapboxgl-track-pointer{cursor:pointer}.mapboxgl-canvas-container.mapboxgl-interactive:active,.mapboxgl-ctrl-group button.mapboxgl-ctrl-compass:active{cursor:grabbing}.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate,.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate .mapboxgl-canvas{touch-action:pan-x pan-y}.mapboxgl-canvas-container.mapboxgl-touch-drag-pan,.mapboxgl-canvas-container.mapboxgl-touch-drag-pan .mapboxgl-canvas{touch-action:pinch-zoom}.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate.mapboxgl-touch-drag-pan,.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate.mapboxgl-touch-drag-pan .mapboxgl-canvas{touch-action:none}.mapboxgl-ctrl-bottom-left,.mapboxgl-ctrl-bottom-right,.mapboxgl-ctrl-top-left,.mapboxgl-ctrl-top-right{pointer-events:none;position:absolute;z-index:2}.mapboxgl-ctrl-top-left{left:0;top:0}.mapboxgl-ctrl-top-right{right:0;top:0}.mapboxgl-ctrl-bottom-left{bottom:0;left:0}.mapboxgl-ctrl-bottom-right{bottom:0;right:0}.mapboxgl-ctrl{clear:both;pointer-events:auto;transform:translate(0)}.mapboxgl-ctrl-top-left .mapboxgl-ctrl{float:left;margin:10px 0 0 10px}.mapboxgl-ctrl-top-right .mapboxgl-ctrl{float:right;margin:10px 10px 0 0}.mapboxgl-ctrl-bottom-left .mapboxgl-ctrl{float:left;margin:0 0 10px 10px}.mapboxgl-ctrl-bottom-right .mapboxgl-ctrl{float:right;margin:0 10px 10px 0}.mapboxgl-ctrl-group{background:#fff;border-radius:4px}.mapboxgl-ctrl-group:not(:empty){box-shadow:0 0 0 2px rgba(0,0,0,.1)}@media (-ms-high-contrast:active){.mapboxgl-ctrl-group:not(:empty){box-shadow:0 0 0 2px ButtonText}}.mapboxgl-ctrl-group button{background-color:transparent;border:0;box-sizing:border-box;cursor:pointer;display:block;height:29px;outline:none;overflow:hidden;padding:0;width:29px}.mapboxgl-ctrl-group button+button{border-top:1px solid #ddd}.mapboxgl-ctrl button .mapboxgl-ctrl-icon{background-position:50%;background-repeat:no-repeat;display:block;height:100%;width:100%}@media (-ms-high-contrast:active){.mapboxgl-ctrl-icon{background-color:transparent}.mapboxgl-ctrl-group button+button{border-top:1px solid ButtonText}}.mapboxgl-ctrl-attrib-button:focus,.mapboxgl-ctrl-group button:focus{box-shadow:0 0 2px 2px #0096ff}.mapboxgl-ctrl button:disabled{cursor:not-allowed}.mapboxgl-ctrl button:disabled .mapboxgl-ctrl-icon{opacity:.25}.mapboxgl-ctrl-group button:first-child{border-radius:4px 4px 0 0}.mapboxgl-ctrl-group button:last-child{border-radius:0 0 4px 4px}.mapboxgl-ctrl-group button:only-child{border-radius:inherit}.mapboxgl-ctrl button:not(:disabled):hover{background-color:rgb(0 0 0/5%)}.mapboxgl-ctrl-group button:focus:focus-visible{box-shadow:0 0 2px 2px #0096ff}.mapboxgl-ctrl-group button:focus:not(:focus-visible){box-shadow:none}.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-out .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-in .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E")}@media (-ms-high-contrast:active){.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-out .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-in .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E")}}@media (-ms-high-contrast:black-on-white){.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-out .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23000' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-in .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23000' viewBox='0 0 29 29'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E")}}.mapboxgl-ctrl button.mapboxgl-ctrl-fullscreen .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3h1zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16h1zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5H13zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1V7.5z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-shrink .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29 29'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1h-5.5zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1v-5.5zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1v5.5zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1h5.5z'/%3E%3C/svg%3E")}@media (-ms-high-contrast:active){.mapboxgl-ctrl button.mapboxgl-ctrl-fullscreen .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3h1zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16h1zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5H13zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1V7.5z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-shrink .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1h-5.5zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1v-5.5zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1v5.5zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1h5.5z'/%3E%3C/svg%3E")}}@media (-ms-high-contrast:black-on-white){.mapboxgl-ctrl button.mapboxgl-ctrl-fullscreen .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23000' viewBox='0 0 29 29'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3h1zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16h1zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5H13zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1V7.5z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-shrink .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23000' viewBox='0 0 29 29'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1h-5.5zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1v-5.5zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1v5.5zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1h5.5z'/%3E%3C/svg%3E")}}.mapboxgl-ctrl button.mapboxgl-ctrl-compass .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M10.5 14l4-8 4 8h-8z'/%3E%3Cpath id='south' d='M10.5 16l4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E")}@media (-ms-high-contrast:active){.mapboxgl-ctrl button.mapboxgl-ctrl-compass .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M10.5 14l4-8 4 8h-8z'/%3E%3Cpath id='south' d='M10.5 16l4 8 4-8h-8z' fill='%23999'/%3E%3C/svg%3E")}}@media (-ms-high-contrast:black-on-white){.mapboxgl-ctrl button.mapboxgl-ctrl-compass .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23000' viewBox='0 0 29 29'%3E%3Cpath d='M10.5 14l4-8 4 8h-8z'/%3E%3Cpath id='south' d='M10.5 16l4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E")}}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate:disabled .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23aaa'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' fill='%23f00'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active-error .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e58978'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2' display='none'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background-error .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e54e33'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2' display='none'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-waiting .mapboxgl-ctrl-icon{animation:mapboxgl-spin 2s linear infinite}@media (-ms-high-contrast:active){.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23fff'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate:disabled .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23999'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' fill='%23f00'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active-error .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e58978'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2' display='none'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background-error .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e54e33'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2' display='none'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")}}@media (-ms-high-contrast:black-on-white){.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23000'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate:disabled .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23666'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' fill='%23f00'/%3E%3C/svg%3E")}}@keyframes mapboxgl-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}a.mapboxgl-ctrl-logo{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' fill-rule='evenodd' viewBox='0 0 88 23'%3E%3Cdefs%3E%3Cpath id='logo' d='M11.5 2.25c5.105 0 9.25 4.145 9.25 9.25s-4.145 9.25-9.25 9.25-9.25-4.145-9.25-9.25 4.145-9.25 9.25-9.25zM6.997 15.983c-.051-.338-.828-5.802 2.233-8.873a4.395 4.395 0 013.13-1.28c1.27 0 2.49.51 3.39 1.42.91.9 1.42 2.12 1.42 3.39 0 1.18-.449 2.301-1.28 3.13C12.72 16.93 7 16 7 16l-.003-.017zM15.3 10.5l-2 .8-.8 2-.8-2-2-.8 2-.8.8-2 .8 2 2 .8z'/%3E%3Cpath id='text' d='M50.63 8c.13 0 .23.1.23.23V9c.7-.76 1.7-1.18 2.73-1.18 2.17 0 3.95 1.85 3.95 4.17s-1.77 4.19-3.94 4.19c-1.04 0-2.03-.43-2.74-1.18v3.77c0 .13-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V8.23c0-.12.1-.23.23-.23h1.4zm-3.86.01c.01 0 .01 0 .01-.01.13 0 .22.1.22.22v7.55c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V15c-.7.76-1.69 1.19-2.73 1.19-2.17 0-3.94-1.87-3.94-4.19 0-2.32 1.77-4.19 3.94-4.19 1.03 0 2.02.43 2.73 1.18v-.75c0-.12.1-.23.23-.23h1.4zm26.375-.19a4.24 4.24 0 00-4.16 3.29c-.13.59-.13 1.19 0 1.77a4.233 4.233 0 004.17 3.3c2.35 0 4.26-1.87 4.26-4.19 0-2.32-1.9-4.17-4.27-4.17zM60.63 5c.13 0 .23.1.23.23v3.76c.7-.76 1.7-1.18 2.73-1.18 1.88 0 3.45 1.4 3.84 3.28.13.59.13 1.2 0 1.8-.39 1.88-1.96 3.29-3.84 3.29-1.03 0-2.02-.43-2.73-1.18v.77c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V5.23c0-.12.1-.23.23-.23h1.4zm-34 11h-1.4c-.13 0-.23-.11-.23-.23V8.22c.01-.13.1-.22.23-.22h1.4c.13 0 .22.11.23.22v.68c.5-.68 1.3-1.09 2.16-1.1h.03c1.09 0 2.09.6 2.6 1.55.45-.95 1.4-1.55 2.44-1.56 1.62 0 2.93 1.25 2.9 2.78l.03 5.2c0 .13-.1.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.8 0-1.46.7-1.59 1.62l.01 4.68c0 .13-.11.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.85 0-1.54.79-1.6 1.8v4.5c0 .13-.1.23-.23.23zm53.615 0h-1.61c-.04 0-.08-.01-.12-.03-.09-.06-.13-.19-.06-.28l2.43-3.71-2.39-3.65a.213.213 0 01-.03-.12c0-.12.09-.21.21-.21h1.61c.13 0 .24.06.3.17l1.41 2.37 1.4-2.37a.34.34 0 01.3-.17h1.6c.04 0 .08.01.12.03.09.06.13.19.06.28l-2.37 3.65 2.43 3.7c0 .05.01.09.01.13 0 .12-.09.21-.21.21h-1.61c-.13 0-.24-.06-.3-.17l-1.44-2.42-1.44 2.42a.34.34 0 01-.3.17zm-7.12-1.49c-1.33 0-2.42-1.12-2.42-2.51 0-1.39 1.08-2.52 2.42-2.52 1.33 0 2.42 1.12 2.42 2.51 0 1.39-1.08 2.51-2.42 2.52zm-19.865 0c-1.32 0-2.39-1.11-2.42-2.48v-.07c.02-1.38 1.09-2.49 2.4-2.49 1.32 0 2.41 1.12 2.41 2.51 0 1.39-1.07 2.52-2.39 2.53zm-8.11-2.48c-.01 1.37-1.09 2.47-2.41 2.47s-2.42-1.12-2.42-2.51c0-1.39 1.08-2.52 2.4-2.52 1.33 0 2.39 1.11 2.41 2.48l.02.08zm18.12 2.47c-1.32 0-2.39-1.11-2.41-2.48v-.06c.02-1.38 1.09-2.48 2.41-2.48s2.42 1.12 2.42 2.51c0 1.39-1.09 2.51-2.42 2.51z'/%3E%3C/defs%3E%3Cmask id='clip'%3E%3Crect x='0' y='0' width='100%25' height='100%25' fill='white'/%3E%3Cuse xlink:href='%23logo'/%3E%3Cuse xlink:href='%23text'/%3E%3C/mask%3E%3Cg id='outline' opacity='0.3' stroke='%23000' stroke-width='3'%3E%3Ccircle mask='url(%23clip)' cx='11.5' cy='11.5' r='9.25'/%3E%3Cuse xlink:href='%23text' mask='url(%23clip)'/%3E%3C/g%3E%3Cg id='fill' opacity='0.9' fill='%23fff'%3E%3Cuse xlink:href='%23logo'/%3E%3Cuse xlink:href='%23text'/%3E%3C/g%3E%3C/svg%3E");background-repeat:no-repeat;cursor:pointer;display:block;height:23px;margin:0 0 -4px -4px;overflow:hidden;width:88px}a.mapboxgl-ctrl-logo.mapboxgl-compact{width:23px}@media (-ms-high-contrast:active){a.mapboxgl-ctrl-logo{background-color:transparent;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' fill-rule='evenodd' viewBox='0 0 88 23'%3E%3Cdefs%3E%3Cpath id='logo' d='M11.5 2.25c5.105 0 9.25 4.145 9.25 9.25s-4.145 9.25-9.25 9.25-9.25-4.145-9.25-9.25 4.145-9.25 9.25-9.25zM6.997 15.983c-.051-.338-.828-5.802 2.233-8.873a4.395 4.395 0 013.13-1.28c1.27 0 2.49.51 3.39 1.42.91.9 1.42 2.12 1.42 3.39 0 1.18-.449 2.301-1.28 3.13C12.72 16.93 7 16 7 16l-.003-.017zM15.3 10.5l-2 .8-.8 2-.8-2-2-.8 2-.8.8-2 .8 2 2 .8z'/%3E%3Cpath id='text' d='M50.63 8c.13 0 .23.1.23.23V9c.7-.76 1.7-1.18 2.73-1.18 2.17 0 3.95 1.85 3.95 4.17s-1.77 4.19-3.94 4.19c-1.04 0-2.03-.43-2.74-1.18v3.77c0 .13-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V8.23c0-.12.1-.23.23-.23h1.4zm-3.86.01c.01 0 .01 0 .01-.01.13 0 .22.1.22.22v7.55c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V15c-.7.76-1.69 1.19-2.73 1.19-2.17 0-3.94-1.87-3.94-4.19 0-2.32 1.77-4.19 3.94-4.19 1.03 0 2.02.43 2.73 1.18v-.75c0-.12.1-.23.23-.23h1.4zm26.375-.19a4.24 4.24 0 00-4.16 3.29c-.13.59-.13 1.19 0 1.77a4.233 4.233 0 004.17 3.3c2.35 0 4.26-1.87 4.26-4.19 0-2.32-1.9-4.17-4.27-4.17zM60.63 5c.13 0 .23.1.23.23v3.76c.7-.76 1.7-1.18 2.73-1.18 1.88 0 3.45 1.4 3.84 3.28.13.59.13 1.2 0 1.8-.39 1.88-1.96 3.29-3.84 3.29-1.03 0-2.02-.43-2.73-1.18v.77c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V5.23c0-.12.1-.23.23-.23h1.4zm-34 11h-1.4c-.13 0-.23-.11-.23-.23V8.22c.01-.13.1-.22.23-.22h1.4c.13 0 .22.11.23.22v.68c.5-.68 1.3-1.09 2.16-1.1h.03c1.09 0 2.09.6 2.6 1.55.45-.95 1.4-1.55 2.44-1.56 1.62 0 2.93 1.25 2.9 2.78l.03 5.2c0 .13-.1.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.8 0-1.46.7-1.59 1.62l.01 4.68c0 .13-.11.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.85 0-1.54.79-1.6 1.8v4.5c0 .13-.1.23-.23.23zm53.615 0h-1.61c-.04 0-.08-.01-.12-.03-.09-.06-.13-.19-.06-.28l2.43-3.71-2.39-3.65a.213.213 0 01-.03-.12c0-.12.09-.21.21-.21h1.61c.13 0 .24.06.3.17l1.41 2.37 1.4-2.37a.34.34 0 01.3-.17h1.6c.04 0 .08.01.12.03.09.06.13.19.06.28l-2.37 3.65 2.43 3.7c0 .05.01.09.01.13 0 .12-.09.21-.21.21h-1.61c-.13 0-.24-.06-.3-.17l-1.44-2.42-1.44 2.42a.34.34 0 01-.3.17zm-7.12-1.49c-1.33 0-2.42-1.12-2.42-2.51 0-1.39 1.08-2.52 2.42-2.52 1.33 0 2.42 1.12 2.42 2.51 0 1.39-1.08 2.51-2.42 2.52zm-19.865 0c-1.32 0-2.39-1.11-2.42-2.48v-.07c.02-1.38 1.09-2.49 2.4-2.49 1.32 0 2.41 1.12 2.41 2.51 0 1.39-1.07 2.52-2.39 2.53zm-8.11-2.48c-.01 1.37-1.09 2.47-2.41 2.47s-2.42-1.12-2.42-2.51c0-1.39 1.08-2.52 2.4-2.52 1.33 0 2.39 1.11 2.41 2.48l.02.08zm18.12 2.47c-1.32 0-2.39-1.11-2.41-2.48v-.06c.02-1.38 1.09-2.48 2.41-2.48s2.42 1.12 2.42 2.51c0 1.39-1.09 2.51-2.42 2.51z'/%3E%3C/defs%3E%3Cmask id='clip'%3E%3Crect x='0' y='0' width='100%25' height='100%25' fill='white'/%3E%3Cuse xlink:href='%23logo'/%3E%3Cuse xlink:href='%23text'/%3E%3C/mask%3E%3Cg id='outline' opacity='1' stroke='%23000' stroke-width='3'%3E%3Ccircle mask='url(%23clip)' cx='11.5' cy='11.5' r='9.25'/%3E%3Cuse xlink:href='%23text' mask='url(%23clip)'/%3E%3C/g%3E%3Cg id='fill' opacity='1' fill='%23fff'%3E%3Cuse xlink:href='%23logo'/%3E%3Cuse xlink:href='%23text'/%3E%3C/g%3E%3C/svg%3E")}}@media (-ms-high-contrast:black-on-white){a.mapboxgl-ctrl-logo{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' fill-rule='evenodd' viewBox='0 0 88 23'%3E%3Cdefs%3E%3Cpath id='logo' d='M11.5 2.25c5.105 0 9.25 4.145 9.25 9.25s-4.145 9.25-9.25 9.25-9.25-4.145-9.25-9.25 4.145-9.25 9.25-9.25zM6.997 15.983c-.051-.338-.828-5.802 2.233-8.873a4.395 4.395 0 013.13-1.28c1.27 0 2.49.51 3.39 1.42.91.9 1.42 2.12 1.42 3.39 0 1.18-.449 2.301-1.28 3.13C12.72 16.93 7 16 7 16l-.003-.017zM15.3 10.5l-2 .8-.8 2-.8-2-2-.8 2-.8.8-2 .8 2 2 .8z'/%3E%3Cpath id='text' d='M50.63 8c.13 0 .23.1.23.23V9c.7-.76 1.7-1.18 2.73-1.18 2.17 0 3.95 1.85 3.95 4.17s-1.77 4.19-3.94 4.19c-1.04 0-2.03-.43-2.74-1.18v3.77c0 .13-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V8.23c0-.12.1-.23.23-.23h1.4zm-3.86.01c.01 0 .01 0 .01-.01.13 0 .22.1.22.22v7.55c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V15c-.7.76-1.69 1.19-2.73 1.19-2.17 0-3.94-1.87-3.94-4.19 0-2.32 1.77-4.19 3.94-4.19 1.03 0 2.02.43 2.73 1.18v-.75c0-.12.1-.23.23-.23h1.4zm26.375-.19a4.24 4.24 0 00-4.16 3.29c-.13.59-.13 1.19 0 1.77a4.233 4.233 0 004.17 3.3c2.35 0 4.26-1.87 4.26-4.19 0-2.32-1.9-4.17-4.27-4.17zM60.63 5c.13 0 .23.1.23.23v3.76c.7-.76 1.7-1.18 2.73-1.18 1.88 0 3.45 1.4 3.84 3.28.13.59.13 1.2 0 1.8-.39 1.88-1.96 3.29-3.84 3.29-1.03 0-2.02-.43-2.73-1.18v.77c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V5.23c0-.12.1-.23.23-.23h1.4zm-34 11h-1.4c-.13 0-.23-.11-.23-.23V8.22c.01-.13.1-.22.23-.22h1.4c.13 0 .22.11.23.22v.68c.5-.68 1.3-1.09 2.16-1.1h.03c1.09 0 2.09.6 2.6 1.55.45-.95 1.4-1.55 2.44-1.56 1.62 0 2.93 1.25 2.9 2.78l.03 5.2c0 .13-.1.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.8 0-1.46.7-1.59 1.62l.01 4.68c0 .13-.11.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.85 0-1.54.79-1.6 1.8v4.5c0 .13-.1.23-.23.23zm53.615 0h-1.61c-.04 0-.08-.01-.12-.03-.09-.06-.13-.19-.06-.28l2.43-3.71-2.39-3.65a.213.213 0 01-.03-.12c0-.12.09-.21.21-.21h1.61c.13 0 .24.06.3.17l1.41 2.37 1.4-2.37a.34.34 0 01.3-.17h1.6c.04 0 .08.01.12.03.09.06.13.19.06.28l-2.37 3.65 2.43 3.7c0 .05.01.09.01.13 0 .12-.09.21-.21.21h-1.61c-.13 0-.24-.06-.3-.17l-1.44-2.42-1.44 2.42a.34.34 0 01-.3.17zm-7.12-1.49c-1.33 0-2.42-1.12-2.42-2.51 0-1.39 1.08-2.52 2.42-2.52 1.33 0 2.42 1.12 2.42 2.51 0 1.39-1.08 2.51-2.42 2.52zm-19.865 0c-1.32 0-2.39-1.11-2.42-2.48v-.07c.02-1.38 1.09-2.49 2.4-2.49 1.32 0 2.41 1.12 2.41 2.51 0 1.39-1.07 2.52-2.39 2.53zm-8.11-2.48c-.01 1.37-1.09 2.47-2.41 2.47s-2.42-1.12-2.42-2.51c0-1.39 1.08-2.52 2.4-2.52 1.33 0 2.39 1.11 2.41 2.48l.02.08zm18.12 2.47c-1.32 0-2.39-1.11-2.41-2.48v-.06c.02-1.38 1.09-2.48 2.41-2.48s2.42 1.12 2.42 2.51c0 1.39-1.09 2.51-2.42 2.51z'/%3E%3C/defs%3E%3Cmask id='clip'%3E%3Crect x='0' y='0' width='100%25' height='100%25' fill='white'/%3E%3Cuse xlink:href='%23logo'/%3E%3Cuse xlink:href='%23text'/%3E%3C/mask%3E%3Cg id='outline' opacity='1' stroke='%23fff' stroke-width='3' fill='%23fff'%3E%3Ccircle mask='url(%23clip)' cx='11.5' cy='11.5' r='9.25'/%3E%3Cuse xlink:href='%23text' mask='url(%23clip)'/%3E%3C/g%3E%3Cg id='fill' opacity='1' fill='%23000'%3E%3Cuse xlink:href='%23logo'/%3E%3Cuse xlink:href='%23text'/%3E%3C/g%3E%3C/svg%3E")}}.mapboxgl-ctrl.mapboxgl-ctrl-attrib{background-color:hsla(0,0%,100%,.5);margin:0;padding:0 5px}@media screen{.mapboxgl-ctrl-attrib.mapboxgl-compact{background-color:#fff;border-radius:12px;box-sizing:content-box;margin:10px;min-height:20px;padding:2px 24px 2px 0;position:relative}.mapboxgl-ctrl-attrib.mapboxgl-compact-show{padding:2px 28px 2px 8px;visibility:visible}.mapboxgl-ctrl-bottom-left>.mapboxgl-ctrl-attrib.mapboxgl-compact-show,.mapboxgl-ctrl-top-left>.mapboxgl-ctrl-attrib.mapboxgl-compact-show{border-radius:12px;padding:2px 8px 2px 28px}.mapboxgl-ctrl-attrib.mapboxgl-compact .mapboxgl-ctrl-attrib-inner{display:none}.mapboxgl-ctrl-attrib-button{background-color:hsla(0,0%,100%,.5);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E");border:0;border-radius:12px;box-sizing:border-box;cursor:pointer;display:none;height:24px;outline:none;position:absolute;right:0;top:0;width:24px}.mapboxgl-ctrl-bottom-left .mapboxgl-ctrl-attrib-button,.mapboxgl-ctrl-top-left .mapboxgl-ctrl-attrib-button{left:0}.mapboxgl-ctrl-attrib.mapboxgl-compact .mapboxgl-ctrl-attrib-button,.mapboxgl-ctrl-attrib.mapboxgl-compact-show .mapboxgl-ctrl-attrib-inner{display:block}.mapboxgl-ctrl-attrib.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button{background-color:rgb(0 0 0/5%)}.mapboxgl-ctrl-bottom-right>.mapboxgl-ctrl-attrib.mapboxgl-compact:after{bottom:0;right:0}.mapboxgl-ctrl-top-right>.mapboxgl-ctrl-attrib.mapboxgl-compact:after{right:0;top:0}.mapboxgl-ctrl-top-left>.mapboxgl-ctrl-attrib.mapboxgl-compact:after{left:0;top:0}.mapboxgl-ctrl-bottom-left>.mapboxgl-ctrl-attrib.mapboxgl-compact:after{bottom:0;left:0}}@media screen and (-ms-high-contrast:active){.mapboxgl-ctrl-attrib.mapboxgl-compact:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd' fill='%23fff'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E")}}@media screen and (-ms-high-contrast:black-on-white){.mapboxgl-ctrl-attrib.mapboxgl-compact:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E")}}.mapboxgl-ctrl-attrib a{color:rgba(0,0,0,.75);text-decoration:none}.mapboxgl-ctrl-attrib a:hover{color:inherit;text-decoration:underline}.mapboxgl-ctrl-attrib .mapbox-improve-map{font-weight:700;margin-left:2px}.mapboxgl-attrib-empty{display:none}.mapboxgl-ctrl-scale{background-color:hsla(0,0%,100%,.75);border:2px solid #333;border-top:#333;box-sizing:border-box;color:#333;font-size:10px;padding:0 5px;white-space:nowrap}.mapboxgl-popup{display:flex;left:0;pointer-events:none;position:absolute;top:0;will-change:transform}.mapboxgl-popup-anchor-top,.mapboxgl-popup-anchor-top-left,.mapboxgl-popup-anchor-top-right{flex-direction:column}.mapboxgl-popup-anchor-bottom,.mapboxgl-popup-anchor-bottom-left,.mapboxgl-popup-anchor-bottom-right{flex-direction:column-reverse}.mapboxgl-popup-anchor-left{flex-direction:row}.mapboxgl-popup-anchor-right{flex-direction:row-reverse}.mapboxgl-popup-tip{border:10px solid transparent;height:0;width:0;z-index:1}.mapboxgl-popup-anchor-top .mapboxgl-popup-tip{align-self:center;border-bottom-color:#fff;border-top:none}.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip{align-self:flex-start;border-bottom-color:#fff;border-left:none;border-top:none}.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip{align-self:flex-end;border-bottom-color:#fff;border-right:none;border-top:none}.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip{align-self:center;border-bottom:none;border-top-color:#fff}.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip{align-self:flex-start;border-bottom:none;border-left:none;border-top-color:#fff}.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip{align-self:flex-end;border-bottom:none;border-right:none;border-top-color:#fff}.mapboxgl-popup-anchor-left .mapboxgl-popup-tip{align-self:center;border-left:none;border-right-color:#fff}.mapboxgl-popup-anchor-right .mapboxgl-popup-tip{align-self:center;border-left-color:#fff;border-right:none}.mapboxgl-popup-close-button{background-color:transparent;border:0;border-radius:0 3px 0 0;cursor:pointer;position:absolute;right:0;top:0}.mapboxgl-popup-close-button:hover{background-color:rgb(0 0 0/5%)}.mapboxgl-popup-content{background:#fff;border-radius:3px;box-shadow:0 1px 2px rgba(0,0,0,.1);padding:10px 10px 15px;pointer-events:auto;position:relative}.mapboxgl-popup-anchor-top-left .mapboxgl-popup-content{border-top-left-radius:0}.mapboxgl-popup-anchor-top-right .mapboxgl-popup-content{border-top-right-radius:0}.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-content{border-bottom-left-radius:0}.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-content{border-bottom-right-radius:0}.mapboxgl-popup-track-pointer{display:none}.mapboxgl-popup-track-pointer *{pointer-events:none;user-select:none}.mapboxgl-map:hover .mapboxgl-popup-track-pointer{display:flex}.mapboxgl-map:active .mapboxgl-popup-track-pointer{display:none}.mapboxgl-marker{left:0;opacity:1;position:absolute;top:0;transition:opacity .2s;will-change:transform}.mapboxgl-user-location-dot,.mapboxgl-user-location-dot:before{background-color:#1da1f2;border-radius:50%;height:15px;width:15px}.mapboxgl-user-location-dot:before{animation:mapboxgl-user-location-dot-pulse 2s infinite;content:"";position:absolute}.mapboxgl-user-location-dot:after{border:2px solid #fff;border-radius:50%;box-shadow:0 0 3px rgba(0,0,0,.35);box-sizing:border-box;content:"";height:19px;left:-2px;position:absolute;top:-2px;width:19px}.mapboxgl-user-location-show-heading .mapboxgl-user-location-heading{height:0;width:0}.mapboxgl-user-location-show-heading .mapboxgl-user-location-heading:after,.mapboxgl-user-location-show-heading .mapboxgl-user-location-heading:before{border-bottom:7.5px solid #4aa1eb;content:"";position:absolute}.mapboxgl-user-location-show-heading .mapboxgl-user-location-heading:before{border-left:7.5px solid transparent;transform:translateY(-28px) skewY(-20deg)}.mapboxgl-user-location-show-heading .mapboxgl-user-location-heading:after{border-right:7.5px solid transparent;transform:translate(7.5px,-28px) skewY(20deg)}@keyframes mapboxgl-user-location-dot-pulse{0%{opacity:1;transform:scale(1)}70%{opacity:0;transform:scale(3)}to{opacity:0;transform:scale(1)}}.mapboxgl-user-location-dot-stale{background-color:#aaa}.mapboxgl-user-location-dot-stale:after{display:none}.mapboxgl-user-location-accuracy-circle{background-color:#1da1f233;border-radius:100%;height:1px;width:1px}.mapboxgl-crosshair,.mapboxgl-crosshair .mapboxgl-interactive,.mapboxgl-crosshair .mapboxgl-interactive:active{cursor:crosshair}.mapboxgl-boxzoom{background:#fff;border:2px dotted #202020;height:0;left:0;opacity:.5;position:absolute;top:0;width:0}@media print{.mapbox-improve-map{display:none}}.mapboxgl-scroll-zoom-blocker,.mapboxgl-touch-pan-blocker{align-items:center;background:rgba(0,0,0,.7);color:#fff;display:flex;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;height:100%;justify-content:center;left:0;opacity:0;pointer-events:none;position:absolute;text-align:center;top:0;transition:opacity .75s ease-in-out;transition-delay:1s;width:100%}.mapboxgl-scroll-zoom-blocker-show,.mapboxgl-touch-pan-blocker-show{opacity:1;transition:opacity .1s ease-in-out}.mapboxgl-canvas-container.mapboxgl-touch-pan-blocker-override.mapboxgl-scrollable-page,.mapboxgl-canvas-container.mapboxgl-touch-pan-blocker-override.mapboxgl-scrollable-page .mapboxgl-canvas{touch-action:pan-x pan-y} 2 | .mapbox-ctrl-compass { 3 | transition: .2s opacity; 4 | } 5 | 6 | .mapbox-ctrl-compass[hidden] { 7 | display: block; 8 | opacity: 0; 9 | } 10 | 11 | .mapbox-ctrl-compass button { 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | } 16 | .mapbox-ctrl-image button { 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | color: #333; 21 | cursor: pointer; 22 | } 23 | 24 | .mapbox-ctrl-image button[hidden] { 25 | display: none; 26 | } 27 | 28 | .mapbox-ctrl-image button.-active { 29 | color: #4264fb; 30 | } 31 | 32 | .mapbox-ctrl-image button:disabled { 33 | opacity: 0.5; 34 | } 35 | 36 | .mapbox-ctrl-image-add { 37 | position: relative; 38 | display: contents; 39 | } 40 | 41 | .mapbox-ctrl-image input[type="file"] { 42 | opacity: 0; 43 | position: absolute; 44 | left: 0; 45 | top: 0; 46 | width: 1px; 47 | height: 1px; 48 | } 49 | .mapbox-ctrl-inspect button { 50 | display: flex; 51 | align-items: center; 52 | justify-content: center; 53 | color: #333; 54 | } 55 | 56 | .mapbox-ctrl-inspect button.-active { 57 | color: #4264fb; 58 | } 59 | 60 | .mapbox-ctrl-inspect-popup { 61 | position: absolute; 62 | padding: 8px; 63 | border-radius: 4px; 64 | background: #fff; 65 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); 66 | font-family: sans-serif; 67 | white-space: nowrap; 68 | transform: translate(-50%, 5px); 69 | } 70 | 71 | .mapbox-ctrl-inspect-popup::before { 72 | content: ''; 73 | width: 0; 74 | height: 0; 75 | position: absolute; 76 | bottom: 100%; 77 | left: 50%; 78 | transform: translate(-50%, 0); 79 | border-left: 5px solid transparent; 80 | border-right: 5px solid transparent; 81 | border-bottom: 5px solid #fff; 82 | } 83 | 84 | .mapbox-ctrl-inspect-popup header { 85 | display: flex; 86 | justify-content: space-between; 87 | align-items: center; 88 | font-size: 12px; 89 | } 90 | 91 | .mapbox-ctrl-inspect-popup nav { 92 | margin: 0 auto; 93 | } 94 | 95 | .mapbox-ctrl-inspect-popup button { 96 | flex: none; 97 | padding: 4px 12px; 98 | border-radius: 4px; 99 | border: none; 100 | background: none; 101 | font-family: sans-serif; 102 | font-size: 16px; 103 | color: #4264fb; 104 | cursor: pointer; 105 | } 106 | 107 | .mapbox-ctrl-inspect-popup table { 108 | width: 100%; 109 | min-width: 200px; 110 | max-width: 400px; 111 | border-collapse: collapse; 112 | } 113 | 114 | .mapbox-ctrl-inspect-popup tr:not(:last-child) td, 115 | .mapbox-ctrl-inspect-popup tr:not(:last-child) th { 116 | border-bottom: 1px solid rgba(0, 0, 0, 0.05); 117 | } 118 | 119 | .mapbox-ctrl-inspect-popup th, 120 | .mapbox-ctrl-inspect-popup td { 121 | width: 50%; 122 | padding: 5px; 123 | white-space: break-spaces; 124 | } 125 | 126 | .mapbox-ctrl-inspect-popup th { 127 | text-align: right; 128 | font-weight: 600; 129 | } 130 | 131 | .mapbox-ctrl-inspect-popup td[colspan="2"] { 132 | text-align: center; 133 | color: #4264fb; 134 | font-weight: 600; 135 | } 136 | .mapbox-ctrl-ruler button { 137 | display: flex; 138 | align-items: center; 139 | justify-content: center; 140 | color: #333; 141 | } 142 | 143 | .mapbox-ctrl-ruler button.-active { 144 | color: #4264fb; 145 | } 146 | .mapbox-ctrl-styles-expanded { 147 | display: flex; 148 | } 149 | 150 | .mapbox-ctrl-styles-expanded button { 151 | width: auto; 152 | padding: 0 8px; 153 | color: #333; 154 | } 155 | 156 | .mapboxgl-ctrl-group.mapbox-ctrl-styles-expanded button { 157 | border-radius: 0; 158 | } 159 | 160 | .mapboxgl-ctrl-group.mapbox-ctrl-styles-expanded button:first-child { 161 | border-radius: 4px 0 0 4px; 162 | } 163 | 164 | .mapboxgl-ctrl-group.mapbox-ctrl-styles-expanded button:last-child { 165 | border-radius: 0 4px 4px 0; 166 | } 167 | 168 | .mapbox-ctrl-styles-expanded button + button { 169 | border: none; 170 | } 171 | 172 | .mapbox-ctrl-styles-expanded button.-active { 173 | background: rgba(0, 0, 0, 0.05); 174 | } 175 | 176 | .mapbox-ctrl-styles-compact button { 177 | position: relative; 178 | display: flex; 179 | align-items: center; 180 | justify-content: center; 181 | color: #333; 182 | } 183 | 184 | .mapbox-ctrl-styles-compact select { 185 | position: absolute; 186 | left: 0; 187 | top: 0; 188 | right: 0; 189 | bottom: 0; 190 | opacity: 0; 191 | cursor: pointer; 192 | } 193 | 194 | .mapbox-ctrl-tooltip { 195 | padding: 5px 7px; 196 | background: #fff; 197 | border-radius: 2px; 198 | position: absolute; 199 | transform: translate(-50%, 0); 200 | margin-top: 24px; 201 | font-size: 14px; 202 | white-space: nowrap; 203 | /* show tooltip over control buttons which z-index is 2 */ 204 | z-index: 3; 205 | } 206 | 207 | .mapbox-ctrl-tooltip:empty { 208 | display: none; 209 | } 210 | .mapbox-ctrl-zoom button { 211 | display: flex; 212 | align-items: center; 213 | justify-content: center; 214 | color: #333; 215 | } -------------------------------------------------------------------------------- /preview/preview.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: sans-serif; 4 | font-size: 15px; 5 | position: fixed; 6 | left: 0; 7 | top: 0; 8 | right: 0; 9 | bottom: 0; 10 | overflow: hidden; 11 | } 12 | 13 | .map { 14 | position: absolute; 15 | left: 0; 16 | top: 0; 17 | right: 0; 18 | bottom: 0; 19 | } 20 | 21 | .languages { 22 | position: absolute; 23 | left: 50%; 24 | top: 16px; 25 | transform: translateX(-50%); 26 | padding: 2px; 27 | z-index: 1; 28 | } 29 | 30 | .github { 31 | position: absolute; 32 | top: 10px; 33 | right: 10px; 34 | } -------------------------------------------------------------------------------- /preview/preview.js: -------------------------------------------------------------------------------- 1 | import mapboxgl from 'mapbox-gl'; 2 | import 'mapbox-gl/dist/mapbox-gl.css'; 3 | /** CompassControl */ 4 | import CompassControl from '@mapbox-controls/compass'; 5 | import '@mapbox-controls/compass/src/index.css'; 6 | /** ImageControl */ 7 | import ImageControl from '@mapbox-controls/image'; 8 | import '@mapbox-controls/image/src/index.css'; 9 | /** InspectControl */ 10 | import InspectControl from '@mapbox-controls/inspect'; 11 | import '@mapbox-controls/inspect/src/index.css'; 12 | /** LanguageControl */ 13 | import LanguageControl from '@mapbox-controls/language'; 14 | /** RulerControl */ 15 | import RulerControl from '@mapbox-controls/ruler'; 16 | import '@mapbox-controls/ruler/src/index.css'; 17 | /** StylesControl */ 18 | import StylesControl from '@mapbox-controls/styles'; 19 | import '@mapbox-controls/styles/src/index.css'; 20 | /** TooltipControl */ 21 | import TooltipControl from '@mapbox-controls/tooltip'; 22 | import '@mapbox-controls/tooltip/src/index.css'; 23 | /** ZoomControl */ 24 | import ZoomControl from '@mapbox-controls/zoom'; 25 | import '@mapbox-controls/zoom/src/index.css'; 26 | 27 | const polygon = { 28 | id: 1234567890, 29 | type: 'Feature', 30 | properties: {}, 31 | geometry: { 32 | type: 'Polygon', 33 | coordinates: [ 34 | [ 35 | [30.51611423492432, 50.452667766971196], 36 | [30.514655113220215, 50.449006093706274], 37 | [30.516843795776367, 50.44862351447756], 38 | [30.518345832824707, 50.45217591688964], 39 | [30.51611423492432, 50.452667766971196], 40 | ], 41 | ], 42 | }, 43 | }; 44 | 45 | const map = new mapboxgl.Map({ 46 | accessToken: 'pk.eyJ1Ijoia29yeXdrYSIsImEiOiJjbTJreGo1bHkwNWx1MmtxdGJtN2phdmEwIn0.Rct4IUBzwzsKF90Riz81dA', 47 | container: 'map', 48 | style: 'mapbox://styles/mapbox/standard', 49 | zoom: 14, 50 | center: [30.5234, 50.4501], 51 | }); 52 | 53 | map.on('style.load', () => { 54 | map.addLayer({ 55 | id: 'polygon-fill', 56 | type: 'fill', 57 | source: { type: 'geojson', data: polygon }, 58 | paint: { 'fill-opacity': 0.2, 'fill-color': '#4264fb' }, 59 | }); 60 | map.addLayer({ 61 | id: 'polygon-line', 62 | type: 'line', 63 | source: { type: 'geojson', data: polygon }, 64 | paint: { 'line-width': 2, 'line-color': '#4264fb' }, 65 | }); 66 | }); 67 | 68 | map.addControl(new ZoomControl(), 'bottom-right'); 69 | 70 | map.addControl(new InspectControl({ console: true }), 'bottom-right'); 71 | 72 | map.addControl(new RulerControl(), 'bottom-right'); 73 | map.on('ruler.on', () => console.log('Ruler activated')); 74 | map.on('ruler.off', () => console.log('Ruler deactivated')); 75 | 76 | const image = new ImageControl({ removeButton: true }); 77 | map.addControl(image, 'bottom-right'); 78 | map.on('image.add', ({ id }) => console.log(`Added image ${id}`)); 79 | map.on('image.remove', ({ id }) => console.log(`Removed image ${id}`)); 80 | map.on('image.select', ({ id }) => console.log(`Selected image ${id}`)); 81 | map.on('image.deselect', ({ id }) => console.log(`Deselected image ${id}`)); 82 | map.on('image.update', ({ coordinates }) => console.log('Updated position:', coordinates)); 83 | map.on('image.mode', ({ mode }) => console.log(`Changed mode: ${mode}`)); 84 | 85 | map.addControl(new CompassControl({ instant: true }), 'bottom-right'); 86 | 87 | (async function () { 88 | await map.once('style.load'); 89 | await image.addUrl('https://korywka.github.io/mapbox-controls/preview/plan.jpg', [ 90 | [ 91 | 30.622053488641882, 92 | 50.43926060648866, 93 | ], 94 | [ 95 | 30.627144888757584, 96 | 50.43197654403531, 97 | ], 98 | [ 99 | 30.617797873099676, 100 | 50.429326551923964, 101 | ], 102 | [ 103 | 30.612705668630156, 104 | 50.436610940291615, 105 | ], 106 | ]); 107 | 108 | map.on('image.select', ({ id }) => { 109 | const rasterLayerId = image.rasters[id].rasterLayer.id; 110 | const range = document.createElement('input'); 111 | range.style.position = 'absolute'; 112 | range.style.left = '50%'; 113 | range.style.transform = 'translateX(-50%)'; 114 | range.style.bottom = '16px'; 115 | range.type = 'range'; 116 | range.min = 0; 117 | range.step = 0.05; 118 | range.max = 1; 119 | range.value = map.getPaintProperty(rasterLayerId, 'raster-opacity'); 120 | range.addEventListener('input', () => { 121 | map.setPaintProperty(rasterLayerId, 'raster-opacity', Number(range.value)); 122 | }); 123 | document.body.appendChild(range); 124 | map.once('image.deselect', () => { 125 | document.body.removeChild(range); 126 | }); 127 | }); 128 | })(); 129 | 130 | map.addControl(new TooltipControl({ 131 | layer: 'polygon-fill', 132 | getContent: (event) => { 133 | console.log('Tooltip for feature id:', event.features?.at(0).id); 134 | return `TooltipControl example ${event.lngLat.lng.toFixed(6)}, ${event.lngLat.lat.toFixed(6)}`; 135 | }, 136 | })); 137 | 138 | const languageControl = new LanguageControl(); 139 | map.addControl(languageControl); 140 | document.getElementById('languages').addEventListener('change', (event) => { 141 | languageControl.setLanguage(event.target.value); 142 | }); 143 | 144 | map.addControl(new StylesControl(), 'top-left'); 145 | map.addControl(new StylesControl({ compact: true }), 'top-left'); 146 | -------------------------------------------------------------------------------- /preview/rollup.config.js: -------------------------------------------------------------------------------- 1 | import css from 'rollup-plugin-import-css'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import polyfills from 'rollup-plugin-polyfill-node'; 5 | 6 | export default { 7 | input: 'preview.js', 8 | output: { 9 | file: 'preview.bundle.js', 10 | format: 'iife', 11 | sourcemap: false, 12 | }, 13 | plugins: [ 14 | polyfills(), 15 | commonjs(), 16 | resolve(), 17 | css({ output: 'preview.bundle.css' }), 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /tsconfig.shared.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "checkJs": true, 5 | "allowJs": true, 6 | "declaration": true, 7 | "skipLibCheck": true, 8 | "emitDeclarationOnly": true, 9 | "esModuleInterop": true, 10 | "removeComments": true, 11 | "lib": [ 12 | "dom", 13 | "esnext" 14 | ], 15 | "outDir": "types" 16 | }, 17 | "include": [ 18 | "src" 19 | ] 20 | } --------------------------------------------------------------------------------