├── .gitignore
├── README.md
├── dist
├── mapboxgl-marker-compass.css
├── mapboxgl-marker-compass.mjs
└── mapboxgl-marker-compass.umd.js
├── index.html
├── package.json
├── pnpm-lock.yaml
├── src
├── MarkerCompass.css
├── MarkerCompass.js
└── main.js
└── vite.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | .vite
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mapbox GL Marker Compass
2 |
3 | Plugin for Mapbox GL JS to create small marker compasses at the edge of the screen to indicate markers outside the viewport.
4 |
5 | 
6 |
7 | # Live Demos
8 |
9 | [Basic usage](https://mapbox-gl-marker-compass.netlify.app)
10 |
11 | [Example project usage](https://boote-bojen-pokale.de/)
12 |
13 | # Instructions
14 |
15 | 1. Add the package
16 |
17 | ```bash
18 | pnpm add mapboxgl-marker-compass # or npm, yarn
19 | ```
20 |
21 | 2. Add CSS, import module, create a map and markers
22 |
23 | ```html
24 |
28 | ```
29 |
30 | ```javascript
31 | import { MarkerCompass } from "mapboxgl-marker-compass/dist/mapboxgl-marker-compass.mjs";
32 |
33 | const map = new mapboxgl.Map({
34 | container: "map",
35 | center: [13.404954, 52.520008],
36 | zoom: 9,
37 | projection: "equirectangular", // Works best with equirectangular maps
38 | });
39 | const markers = [
40 | new mapboxgl.Marker().setLngLat([13.404954, 52.520008]).addTo(map),
41 | ];
42 | ```
43 |
44 | 3. Pass the map and markers to the `MarkerCompass` constructor
45 |
46 | ```javascript
47 | new MarkerCompass(map, markers, {
48 | // Options, see below
49 | });
50 | ```
51 |
52 | # Options
53 |
54 | | Option | Default | Description |
55 | | ----------------- | --------- | -------------------------------------------------------- |
56 | | `offset` | `10` | Offset of the compass element to the viewport edge in px |
57 | | `width` | `20` | Width of the compass element in px |
58 | | `height` | `20` | Height of the compass element in px |
59 | | `backgroundColor` | `#3FB1CE` | Background color of the compass element and arrow |
60 | | `arrowSize` | `4` | Size of the arrow in px |
61 | | `arrowOffset` | `14` | Offset of the arrow to the compass element in px |
62 | | `flyToZoom` | `12` | Zoom level when clicking on the compass element in px |
63 |
64 | # ⚠️ Note
65 |
66 | Works best with `equirectangular` map projection
67 |
68 | # Copyright
69 |
70 | © 2024 Marco Land
71 |
72 | # License
73 |
74 | AGPL-3.0
75 |
--------------------------------------------------------------------------------
/dist/mapboxgl-marker-compass.css:
--------------------------------------------------------------------------------
1 | .mapboxgl-compass {
2 | -webkit-transition: opacity 400ms ease;
3 | -o-transition: opacity 400ms ease;
4 | transition: opacity 400ms ease;
5 | border-radius: 100%;
6 | cursor: pointer;
7 | background-color: var(--marker-compass-background-color, #3fb1ce);
8 | }
9 | .mapboxgl-compass .mapboxgl-compass__arrow {
10 | height: 100%;
11 | left: 50%;
12 | position: absolute;
13 | top: 50%;
14 | -webkit-transform: translate(-50%, -50%) rotate(var(--marker-compass-angle));
15 | -ms-transform: translate(-50%, -50%) rotate(var(--marker-compass-angle));
16 | transform: translate(-50%, -50%) rotate(var(--marker-compass-angle));
17 | width: 100%;
18 | }
19 | .mapboxgl-compass .mapboxgl-compass__arrow:after {
20 | content: "";
21 | border-bottom: var(--marker-compass-arrow-size, 4px) solid transparent;
22 | border-right: var(--marker-compass-arrow-size, 4px) solid
23 | var(--marker-compass-background-color, #3fb1ce);
24 | border-top: var(--marker-compass-arrow-size, 4px) solid transparent;
25 | height: 0;
26 | left: 50%;
27 | position: absolute;
28 | top: 50%;
29 | -webkit-transform: translate(
30 | calc(-50% - var(--marker-compass-arrow-offset, 14px)),
31 | -50%
32 | );
33 | -ms-transform: translate(
34 | calc(-50% - var(--marker-compass-arrow-offset, 14px)),
35 | -50%
36 | );
37 | transform: translate(
38 | calc(-50% - var(--marker-compass-arrow-offset, 14px)),
39 | -50%
40 | );
41 | width: 0;
42 | }
43 |
--------------------------------------------------------------------------------
/dist/mapboxgl-marker-compass.mjs:
--------------------------------------------------------------------------------
1 | class MarkerCompass {
2 | constructor(map, markers, options) {
3 | this.options = {
4 | // defaults
5 | offset: 10,
6 | width: 20,
7 | height: 20,
8 | backgroundColor: "#3FB1CE",
9 | arrowSize: 4,
10 | arrowOffset: 14,
11 | flyToZoom: 12,
12 | ...options
13 | };
14 | this.container = null;
15 | this.markers = markers;
16 | this.compasses = [];
17 | this.map = map;
18 | this.init();
19 | }
20 | init() {
21 | if (!this.map) {
22 | console.warn("No map container provided.");
23 | return;
24 | }
25 | if (!this.markers || !this.markers.length) {
26 | console.warn("No markers found.");
27 | return;
28 | }
29 | this.map.on("load", this.createCompasses.bind(this));
30 | this.map.on("move", this.updateCompasses.bind(this));
31 | }
32 | destroy() {
33 | this.map.off("move", this.updateCompasses.bind(this));
34 | this.compasses.forEach((compass) => compass.remove());
35 | }
36 | createCompasses() {
37 | const container = this.map.getContainer();
38 | container.style.setProperty(
39 | "--marker-compass-arrow-size",
40 | this.options.arrowSize + "px"
41 | );
42 | container.style.setProperty(
43 | "--marker-compass-arrow-offset",
44 | this.options.arrowOffset + "px"
45 | );
46 | container.style.setProperty(
47 | "--marker-compass-background-color",
48 | this.options.backgroundColor
49 | );
50 | if (!container) {
51 | console.warn("No map container found.");
52 | return;
53 | }
54 | this.markers.forEach((marker, index) => {
55 | const compass = document.createElement("div");
56 | compass.classList.add("mapboxgl-compass");
57 | compass.dataset.compass = "true";
58 | compass.style.width = this.options.width + "px";
59 | compass.style.height = this.options.height + "px";
60 | compass.style.position = "absolute";
61 | compass.style.opacity = 0;
62 | compass.style.zIndex = 2;
63 | const arrow = document.createElement("div");
64 | arrow.classList.add("mapboxgl-compass__arrow");
65 | compass.appendChild(arrow);
66 | container.appendChild(compass);
67 | compass.addEventListener("click", () => {
68 | var _a, _b;
69 | if (!((_b = (_a = this.markers) == null ? void 0 : _a[index]) == null ? void 0 : _b._lngLat)) {
70 | return;
71 | }
72 | this.map.flyTo({
73 | center: this.markers[index]._lngLat,
74 | zoom: this.options.flyToZoom
75 | });
76 | });
77 | this.compasses.push(compass);
78 | });
79 | this.updateCompasses();
80 | }
81 | updateCompasses() {
82 | const bounds = this.map.getBounds();
83 | if (!this.markers.length) {
84 | return;
85 | }
86 | this.markers.forEach((marker, index) => {
87 | const lngLat = marker.getLngLat();
88 | const compass = this.compasses[index];
89 | if (!compass) {
90 | return;
91 | }
92 | const compassWidth = compass.clientWidth;
93 | const compassHeight = compass.clientHeight;
94 | const offset = this.options.offset;
95 | const container = this.map.getContainer();
96 | const maxWidth = container.clientWidth - offset;
97 | const maxHeight = container.clientHeight - offset;
98 | const x = this.map.project(lngLat).x;
99 | const y = this.map.project(lngLat).y;
100 | let translate = {
101 | x: offset,
102 | y: offset
103 | };
104 | if (y > compassHeight) {
105 | translate.y = y - compassHeight / 2;
106 | }
107 | if (y >= maxHeight - compassHeight / 2) {
108 | translate.y = maxHeight - compassHeight;
109 | }
110 | if (x > compassWidth) {
111 | translate.x = x - compassWidth / 2;
112 | }
113 | if (x >= maxWidth - compassWidth / 2) {
114 | translate.x = maxWidth - compassWidth;
115 | }
116 | compass.style.transform = `translate(${translate.x}px, ${translate.y}px)`;
117 | if (!bounds.contains(lngLat)) {
118 | const angleDeg = Math.atan2(translate.y - y, translate.x - x) * 180 / Math.PI;
119 | compass.style.setProperty("--marker-compass-angle", `${angleDeg}deg`);
120 | compass.style.opacity = 1;
121 | compass.style.pointerEvents = "all";
122 | } else {
123 | compass.style.pointerEvents = "none";
124 | compass.style.opacity = 0;
125 | }
126 | });
127 | }
128 | }
129 | export {
130 | MarkerCompass
131 | };
132 |
--------------------------------------------------------------------------------
/dist/mapboxgl-marker-compass.umd.js:
--------------------------------------------------------------------------------
1 | (function(global, factory) {
2 | typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global["mapboxgl-marker-compass"] = {}));
3 | })(this, function(exports2) {
4 | "use strict";
5 | class MarkerCompass {
6 | constructor(map, markers, options) {
7 | this.options = {
8 | // defaults
9 | offset: 10,
10 | width: 20,
11 | height: 20,
12 | backgroundColor: "#3FB1CE",
13 | arrowSize: 4,
14 | arrowOffset: 14,
15 | flyToZoom: 12,
16 | ...options
17 | };
18 | this.container = null;
19 | this.markers = markers;
20 | this.compasses = [];
21 | this.map = map;
22 | this.init();
23 | }
24 | init() {
25 | if (!this.map) {
26 | console.warn("No map container provided.");
27 | return;
28 | }
29 | if (!this.markers || !this.markers.length) {
30 | console.warn("No markers found.");
31 | return;
32 | }
33 | this.map.on("load", this.createCompasses.bind(this));
34 | this.map.on("move", this.updateCompasses.bind(this));
35 | }
36 | destroy() {
37 | this.map.off("move", this.updateCompasses.bind(this));
38 | this.compasses.forEach((compass) => compass.remove());
39 | }
40 | createCompasses() {
41 | const container = this.map.getContainer();
42 | container.style.setProperty(
43 | "--marker-compass-arrow-size",
44 | this.options.arrowSize + "px"
45 | );
46 | container.style.setProperty(
47 | "--marker-compass-arrow-offset",
48 | this.options.arrowOffset + "px"
49 | );
50 | container.style.setProperty(
51 | "--marker-compass-background-color",
52 | this.options.backgroundColor
53 | );
54 | if (!container) {
55 | console.warn("No map container found.");
56 | return;
57 | }
58 | this.markers.forEach((marker, index) => {
59 | const compass = document.createElement("div");
60 | compass.classList.add("mapboxgl-compass");
61 | compass.dataset.compass = "true";
62 | compass.style.width = this.options.width + "px";
63 | compass.style.height = this.options.height + "px";
64 | compass.style.position = "absolute";
65 | compass.style.opacity = 0;
66 | compass.style.zIndex = 2;
67 | const arrow = document.createElement("div");
68 | arrow.classList.add("mapboxgl-compass__arrow");
69 | compass.appendChild(arrow);
70 | container.appendChild(compass);
71 | compass.addEventListener("click", () => {
72 | var _a, _b;
73 | if (!((_b = (_a = this.markers) == null ? void 0 : _a[index]) == null ? void 0 : _b._lngLat)) {
74 | return;
75 | }
76 | this.map.flyTo({
77 | center: this.markers[index]._lngLat,
78 | zoom: this.options.flyToZoom
79 | });
80 | });
81 | this.compasses.push(compass);
82 | });
83 | this.updateCompasses();
84 | }
85 | updateCompasses() {
86 | const bounds = this.map.getBounds();
87 | if (!this.markers.length) {
88 | return;
89 | }
90 | this.markers.forEach((marker, index) => {
91 | const lngLat = marker.getLngLat();
92 | const compass = this.compasses[index];
93 | if (!compass) {
94 | return;
95 | }
96 | const compassWidth = compass.clientWidth;
97 | const compassHeight = compass.clientHeight;
98 | const offset = this.options.offset;
99 | const container = this.map.getContainer();
100 | const maxWidth = container.clientWidth - offset;
101 | const maxHeight = container.clientHeight - offset;
102 | const x = this.map.project(lngLat).x;
103 | const y = this.map.project(lngLat).y;
104 | let translate = {
105 | x: offset,
106 | y: offset
107 | };
108 | if (y > compassHeight) {
109 | translate.y = y - compassHeight / 2;
110 | }
111 | if (y >= maxHeight - compassHeight / 2) {
112 | translate.y = maxHeight - compassHeight;
113 | }
114 | if (x > compassWidth) {
115 | translate.x = x - compassWidth / 2;
116 | }
117 | if (x >= maxWidth - compassWidth / 2) {
118 | translate.x = maxWidth - compassWidth;
119 | }
120 | compass.style.transform = `translate(${translate.x}px, ${translate.y}px)`;
121 | if (!bounds.contains(lngLat)) {
122 | const angleDeg = Math.atan2(translate.y - y, translate.x - x) * 180 / Math.PI;
123 | compass.style.setProperty("--marker-compass-angle", `${angleDeg}deg`);
124 | compass.style.opacity = 1;
125 | compass.style.pointerEvents = "all";
126 | } else {
127 | compass.style.pointerEvents = "none";
128 | compass.style.opacity = 0;
129 | }
130 | });
131 | }
132 | }
133 | exports2.MarkerCompass = MarkerCompass;
134 | Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
135 | });
136 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Mapbox GL Marker Compass Example
6 |
10 |
14 |
18 |
19 |
20 |
32 |
33 |
34 |
35 |
36 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mapboxgl-marker-compass",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "vite build",
7 | "build:watch": "vite build --watch",
8 | "preview": "vite preview"
9 | },
10 | "devDependencies": {
11 | "vite": "^5.1.0"
12 | },
13 | "dependencies": {
14 | "prettier": "^3.2.5"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '6.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | dependencies:
8 | prettier:
9 | specifier: ^3.2.5
10 | version: 3.2.5
11 |
12 | devDependencies:
13 | vite:
14 | specifier: ^5.1.0
15 | version: 5.1.1
16 |
17 | packages:
18 |
19 | /@esbuild/aix-ppc64@0.19.12:
20 | resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==}
21 | engines: {node: '>=12'}
22 | cpu: [ppc64]
23 | os: [aix]
24 | requiresBuild: true
25 | dev: true
26 | optional: true
27 |
28 | /@esbuild/android-arm64@0.19.12:
29 | resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==}
30 | engines: {node: '>=12'}
31 | cpu: [arm64]
32 | os: [android]
33 | requiresBuild: true
34 | dev: true
35 | optional: true
36 |
37 | /@esbuild/android-arm@0.19.12:
38 | resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==}
39 | engines: {node: '>=12'}
40 | cpu: [arm]
41 | os: [android]
42 | requiresBuild: true
43 | dev: true
44 | optional: true
45 |
46 | /@esbuild/android-x64@0.19.12:
47 | resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==}
48 | engines: {node: '>=12'}
49 | cpu: [x64]
50 | os: [android]
51 | requiresBuild: true
52 | dev: true
53 | optional: true
54 |
55 | /@esbuild/darwin-arm64@0.19.12:
56 | resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==}
57 | engines: {node: '>=12'}
58 | cpu: [arm64]
59 | os: [darwin]
60 | requiresBuild: true
61 | dev: true
62 | optional: true
63 |
64 | /@esbuild/darwin-x64@0.19.12:
65 | resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==}
66 | engines: {node: '>=12'}
67 | cpu: [x64]
68 | os: [darwin]
69 | requiresBuild: true
70 | dev: true
71 | optional: true
72 |
73 | /@esbuild/freebsd-arm64@0.19.12:
74 | resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==}
75 | engines: {node: '>=12'}
76 | cpu: [arm64]
77 | os: [freebsd]
78 | requiresBuild: true
79 | dev: true
80 | optional: true
81 |
82 | /@esbuild/freebsd-x64@0.19.12:
83 | resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==}
84 | engines: {node: '>=12'}
85 | cpu: [x64]
86 | os: [freebsd]
87 | requiresBuild: true
88 | dev: true
89 | optional: true
90 |
91 | /@esbuild/linux-arm64@0.19.12:
92 | resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==}
93 | engines: {node: '>=12'}
94 | cpu: [arm64]
95 | os: [linux]
96 | requiresBuild: true
97 | dev: true
98 | optional: true
99 |
100 | /@esbuild/linux-arm@0.19.12:
101 | resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==}
102 | engines: {node: '>=12'}
103 | cpu: [arm]
104 | os: [linux]
105 | requiresBuild: true
106 | dev: true
107 | optional: true
108 |
109 | /@esbuild/linux-ia32@0.19.12:
110 | resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==}
111 | engines: {node: '>=12'}
112 | cpu: [ia32]
113 | os: [linux]
114 | requiresBuild: true
115 | dev: true
116 | optional: true
117 |
118 | /@esbuild/linux-loong64@0.19.12:
119 | resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==}
120 | engines: {node: '>=12'}
121 | cpu: [loong64]
122 | os: [linux]
123 | requiresBuild: true
124 | dev: true
125 | optional: true
126 |
127 | /@esbuild/linux-mips64el@0.19.12:
128 | resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==}
129 | engines: {node: '>=12'}
130 | cpu: [mips64el]
131 | os: [linux]
132 | requiresBuild: true
133 | dev: true
134 | optional: true
135 |
136 | /@esbuild/linux-ppc64@0.19.12:
137 | resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==}
138 | engines: {node: '>=12'}
139 | cpu: [ppc64]
140 | os: [linux]
141 | requiresBuild: true
142 | dev: true
143 | optional: true
144 |
145 | /@esbuild/linux-riscv64@0.19.12:
146 | resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==}
147 | engines: {node: '>=12'}
148 | cpu: [riscv64]
149 | os: [linux]
150 | requiresBuild: true
151 | dev: true
152 | optional: true
153 |
154 | /@esbuild/linux-s390x@0.19.12:
155 | resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==}
156 | engines: {node: '>=12'}
157 | cpu: [s390x]
158 | os: [linux]
159 | requiresBuild: true
160 | dev: true
161 | optional: true
162 |
163 | /@esbuild/linux-x64@0.19.12:
164 | resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==}
165 | engines: {node: '>=12'}
166 | cpu: [x64]
167 | os: [linux]
168 | requiresBuild: true
169 | dev: true
170 | optional: true
171 |
172 | /@esbuild/netbsd-x64@0.19.12:
173 | resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==}
174 | engines: {node: '>=12'}
175 | cpu: [x64]
176 | os: [netbsd]
177 | requiresBuild: true
178 | dev: true
179 | optional: true
180 |
181 | /@esbuild/openbsd-x64@0.19.12:
182 | resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==}
183 | engines: {node: '>=12'}
184 | cpu: [x64]
185 | os: [openbsd]
186 | requiresBuild: true
187 | dev: true
188 | optional: true
189 |
190 | /@esbuild/sunos-x64@0.19.12:
191 | resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==}
192 | engines: {node: '>=12'}
193 | cpu: [x64]
194 | os: [sunos]
195 | requiresBuild: true
196 | dev: true
197 | optional: true
198 |
199 | /@esbuild/win32-arm64@0.19.12:
200 | resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==}
201 | engines: {node: '>=12'}
202 | cpu: [arm64]
203 | os: [win32]
204 | requiresBuild: true
205 | dev: true
206 | optional: true
207 |
208 | /@esbuild/win32-ia32@0.19.12:
209 | resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==}
210 | engines: {node: '>=12'}
211 | cpu: [ia32]
212 | os: [win32]
213 | requiresBuild: true
214 | dev: true
215 | optional: true
216 |
217 | /@esbuild/win32-x64@0.19.12:
218 | resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==}
219 | engines: {node: '>=12'}
220 | cpu: [x64]
221 | os: [win32]
222 | requiresBuild: true
223 | dev: true
224 | optional: true
225 |
226 | /@rollup/rollup-android-arm-eabi@4.9.6:
227 | resolution: {integrity: sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==}
228 | cpu: [arm]
229 | os: [android]
230 | requiresBuild: true
231 | dev: true
232 | optional: true
233 |
234 | /@rollup/rollup-android-arm64@4.9.6:
235 | resolution: {integrity: sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==}
236 | cpu: [arm64]
237 | os: [android]
238 | requiresBuild: true
239 | dev: true
240 | optional: true
241 |
242 | /@rollup/rollup-darwin-arm64@4.9.6:
243 | resolution: {integrity: sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==}
244 | cpu: [arm64]
245 | os: [darwin]
246 | requiresBuild: true
247 | dev: true
248 | optional: true
249 |
250 | /@rollup/rollup-darwin-x64@4.9.6:
251 | resolution: {integrity: sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==}
252 | cpu: [x64]
253 | os: [darwin]
254 | requiresBuild: true
255 | dev: true
256 | optional: true
257 |
258 | /@rollup/rollup-linux-arm-gnueabihf@4.9.6:
259 | resolution: {integrity: sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==}
260 | cpu: [arm]
261 | os: [linux]
262 | requiresBuild: true
263 | dev: true
264 | optional: true
265 |
266 | /@rollup/rollup-linux-arm64-gnu@4.9.6:
267 | resolution: {integrity: sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==}
268 | cpu: [arm64]
269 | os: [linux]
270 | requiresBuild: true
271 | dev: true
272 | optional: true
273 |
274 | /@rollup/rollup-linux-arm64-musl@4.9.6:
275 | resolution: {integrity: sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==}
276 | cpu: [arm64]
277 | os: [linux]
278 | requiresBuild: true
279 | dev: true
280 | optional: true
281 |
282 | /@rollup/rollup-linux-riscv64-gnu@4.9.6:
283 | resolution: {integrity: sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==}
284 | cpu: [riscv64]
285 | os: [linux]
286 | requiresBuild: true
287 | dev: true
288 | optional: true
289 |
290 | /@rollup/rollup-linux-x64-gnu@4.9.6:
291 | resolution: {integrity: sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==}
292 | cpu: [x64]
293 | os: [linux]
294 | requiresBuild: true
295 | dev: true
296 | optional: true
297 |
298 | /@rollup/rollup-linux-x64-musl@4.9.6:
299 | resolution: {integrity: sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==}
300 | cpu: [x64]
301 | os: [linux]
302 | requiresBuild: true
303 | dev: true
304 | optional: true
305 |
306 | /@rollup/rollup-win32-arm64-msvc@4.9.6:
307 | resolution: {integrity: sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==}
308 | cpu: [arm64]
309 | os: [win32]
310 | requiresBuild: true
311 | dev: true
312 | optional: true
313 |
314 | /@rollup/rollup-win32-ia32-msvc@4.9.6:
315 | resolution: {integrity: sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==}
316 | cpu: [ia32]
317 | os: [win32]
318 | requiresBuild: true
319 | dev: true
320 | optional: true
321 |
322 | /@rollup/rollup-win32-x64-msvc@4.9.6:
323 | resolution: {integrity: sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==}
324 | cpu: [x64]
325 | os: [win32]
326 | requiresBuild: true
327 | dev: true
328 | optional: true
329 |
330 | /@types/estree@1.0.5:
331 | resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
332 | dev: true
333 |
334 | /esbuild@0.19.12:
335 | resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==}
336 | engines: {node: '>=12'}
337 | hasBin: true
338 | requiresBuild: true
339 | optionalDependencies:
340 | '@esbuild/aix-ppc64': 0.19.12
341 | '@esbuild/android-arm': 0.19.12
342 | '@esbuild/android-arm64': 0.19.12
343 | '@esbuild/android-x64': 0.19.12
344 | '@esbuild/darwin-arm64': 0.19.12
345 | '@esbuild/darwin-x64': 0.19.12
346 | '@esbuild/freebsd-arm64': 0.19.12
347 | '@esbuild/freebsd-x64': 0.19.12
348 | '@esbuild/linux-arm': 0.19.12
349 | '@esbuild/linux-arm64': 0.19.12
350 | '@esbuild/linux-ia32': 0.19.12
351 | '@esbuild/linux-loong64': 0.19.12
352 | '@esbuild/linux-mips64el': 0.19.12
353 | '@esbuild/linux-ppc64': 0.19.12
354 | '@esbuild/linux-riscv64': 0.19.12
355 | '@esbuild/linux-s390x': 0.19.12
356 | '@esbuild/linux-x64': 0.19.12
357 | '@esbuild/netbsd-x64': 0.19.12
358 | '@esbuild/openbsd-x64': 0.19.12
359 | '@esbuild/sunos-x64': 0.19.12
360 | '@esbuild/win32-arm64': 0.19.12
361 | '@esbuild/win32-ia32': 0.19.12
362 | '@esbuild/win32-x64': 0.19.12
363 | dev: true
364 |
365 | /fsevents@2.3.3:
366 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
367 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
368 | os: [darwin]
369 | requiresBuild: true
370 | dev: true
371 | optional: true
372 |
373 | /nanoid@3.3.7:
374 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
375 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
376 | hasBin: true
377 | dev: true
378 |
379 | /picocolors@1.0.0:
380 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
381 | dev: true
382 |
383 | /postcss@8.4.35:
384 | resolution: {integrity: sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==}
385 | engines: {node: ^10 || ^12 || >=14}
386 | dependencies:
387 | nanoid: 3.3.7
388 | picocolors: 1.0.0
389 | source-map-js: 1.0.2
390 | dev: true
391 |
392 | /prettier@3.2.5:
393 | resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==}
394 | engines: {node: '>=14'}
395 | hasBin: true
396 | dev: false
397 |
398 | /rollup@4.9.6:
399 | resolution: {integrity: sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==}
400 | engines: {node: '>=18.0.0', npm: '>=8.0.0'}
401 | hasBin: true
402 | dependencies:
403 | '@types/estree': 1.0.5
404 | optionalDependencies:
405 | '@rollup/rollup-android-arm-eabi': 4.9.6
406 | '@rollup/rollup-android-arm64': 4.9.6
407 | '@rollup/rollup-darwin-arm64': 4.9.6
408 | '@rollup/rollup-darwin-x64': 4.9.6
409 | '@rollup/rollup-linux-arm-gnueabihf': 4.9.6
410 | '@rollup/rollup-linux-arm64-gnu': 4.9.6
411 | '@rollup/rollup-linux-arm64-musl': 4.9.6
412 | '@rollup/rollup-linux-riscv64-gnu': 4.9.6
413 | '@rollup/rollup-linux-x64-gnu': 4.9.6
414 | '@rollup/rollup-linux-x64-musl': 4.9.6
415 | '@rollup/rollup-win32-arm64-msvc': 4.9.6
416 | '@rollup/rollup-win32-ia32-msvc': 4.9.6
417 | '@rollup/rollup-win32-x64-msvc': 4.9.6
418 | fsevents: 2.3.3
419 | dev: true
420 |
421 | /source-map-js@1.0.2:
422 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
423 | engines: {node: '>=0.10.0'}
424 | dev: true
425 |
426 | /vite@5.1.1:
427 | resolution: {integrity: sha512-wclpAgY3F1tR7t9LL5CcHC41YPkQIpKUGeIuT8MdNwNZr6OqOTLs7JX5vIHAtzqLWXts0T+GDrh9pN2arneKqg==}
428 | engines: {node: ^18.0.0 || >=20.0.0}
429 | hasBin: true
430 | peerDependencies:
431 | '@types/node': ^18.0.0 || >=20.0.0
432 | less: '*'
433 | lightningcss: ^1.21.0
434 | sass: '*'
435 | stylus: '*'
436 | sugarss: '*'
437 | terser: ^5.4.0
438 | peerDependenciesMeta:
439 | '@types/node':
440 | optional: true
441 | less:
442 | optional: true
443 | lightningcss:
444 | optional: true
445 | sass:
446 | optional: true
447 | stylus:
448 | optional: true
449 | sugarss:
450 | optional: true
451 | terser:
452 | optional: true
453 | dependencies:
454 | esbuild: 0.19.12
455 | postcss: 8.4.35
456 | rollup: 4.9.6
457 | optionalDependencies:
458 | fsevents: 2.3.3
459 | dev: true
460 |
--------------------------------------------------------------------------------
/src/MarkerCompass.css:
--------------------------------------------------------------------------------
1 | .mapboxgl-compass {
2 | -webkit-transition: opacity 400ms ease;
3 | -o-transition: opacity 400ms ease;
4 | transition: opacity 400ms ease;
5 | border-radius: 100%;
6 | cursor: pointer;
7 | background-color: var(--marker-compass-background-color, #3fb1ce);
8 | }
9 | .mapboxgl-compass .mapboxgl-compass__arrow {
10 | height: 100%;
11 | left: 50%;
12 | position: absolute;
13 | top: 50%;
14 | -webkit-transform: translate(-50%, -50%) rotate(var(--marker-compass-angle));
15 | -ms-transform: translate(-50%, -50%) rotate(var(--marker-compass-angle));
16 | transform: translate(-50%, -50%) rotate(var(--marker-compass-angle));
17 | width: 100%;
18 | }
19 | .mapboxgl-compass .mapboxgl-compass__arrow:after {
20 | content: "";
21 | border-bottom: var(--marker-compass-arrow-size, 4px) solid transparent;
22 | border-right: var(--marker-compass-arrow-size, 4px) solid
23 | var(--marker-compass-background-color, #3fb1ce);
24 | border-top: var(--marker-compass-arrow-size, 4px) solid transparent;
25 | height: 0;
26 | left: 50%;
27 | position: absolute;
28 | top: 50%;
29 | -webkit-transform: translate(
30 | calc(-50% - var(--marker-compass-arrow-offset, 14px)),
31 | -50%
32 | );
33 | -ms-transform: translate(
34 | calc(-50% - var(--marker-compass-arrow-offset, 14px)),
35 | -50%
36 | );
37 | transform: translate(
38 | calc(-50% - var(--marker-compass-arrow-offset, 14px)),
39 | -50%
40 | );
41 | width: 0;
42 | }
43 |
--------------------------------------------------------------------------------
/src/MarkerCompass.js:
--------------------------------------------------------------------------------
1 | export class MarkerCompass {
2 | constructor(map, markers, options) {
3 | this.options = {
4 | // defaults
5 | offset: 10,
6 | width: 20,
7 | height: 20,
8 | backgroundColor: "#3FB1CE",
9 | arrowSize: 4,
10 | arrowOffset: 14,
11 | flyToZoom: 12,
12 | ...options,
13 | };
14 | this.container = null;
15 | this.markers = markers;
16 | this.compasses = [];
17 | this.map = map;
18 | this.init();
19 | }
20 |
21 | init() {
22 | if (!this.map) {
23 | console.warn("No map container provided.");
24 | return;
25 | }
26 | if (!this.markers || !this.markers.length) {
27 | console.warn("No markers found.");
28 | return;
29 | }
30 |
31 | this.map.on("load", this.createCompasses.bind(this));
32 | this.map.on("move", this.updateCompasses.bind(this));
33 | }
34 | destroy() {
35 | this.map.off("move", this.updateCompasses.bind(this));
36 | this.compasses.forEach((compass) => compass.remove());
37 | }
38 | createCompasses() {
39 | const container = this.map.getContainer();
40 | container.style.setProperty(
41 | "--marker-compass-arrow-size",
42 | this.options.arrowSize + "px"
43 | );
44 | container.style.setProperty(
45 | "--marker-compass-arrow-offset",
46 | this.options.arrowOffset + "px"
47 | );
48 | container.style.setProperty(
49 | "--marker-compass-background-color",
50 | this.options.backgroundColor
51 | );
52 |
53 | if (!container) {
54 | console.warn("No map container found.");
55 | return;
56 | }
57 | this.markers.forEach((marker, index) => {
58 | const compass = document.createElement("div");
59 | compass.classList.add("mapboxgl-compass");
60 | compass.dataset.compass = "true";
61 | compass.style.width = this.options.width + "px";
62 | compass.style.height = this.options.height + "px";
63 | compass.style.position = "absolute";
64 | compass.style.opacity = 0;
65 | compass.style.zIndex = 2;
66 | const arrow = document.createElement("div");
67 | arrow.classList.add("mapboxgl-compass__arrow");
68 | compass.appendChild(arrow);
69 | container.appendChild(compass);
70 | compass.addEventListener("click", () => {
71 | if (!this.markers?.[index]?._lngLat) {
72 | return;
73 | }
74 | this.map.flyTo({
75 | center: this.markers[index]._lngLat,
76 | zoom: this.options.flyToZoom,
77 | });
78 | });
79 | this.compasses.push(compass);
80 | });
81 | this.updateCompasses();
82 | }
83 | updateCompasses() {
84 | const bounds = this.map.getBounds();
85 | if (!this.markers.length) {
86 | return;
87 | }
88 | this.markers.forEach((marker, index) => {
89 | const lngLat = marker.getLngLat();
90 | const compass = this.compasses[index];
91 | if (!compass) {
92 | return;
93 | }
94 | const compassWidth = compass.clientWidth;
95 | const compassHeight = compass.clientHeight;
96 | const offset = this.options.offset;
97 | const container = this.map.getContainer();
98 | const maxWidth = container.clientWidth - offset;
99 | const maxHeight = container.clientHeight - offset;
100 | const x = this.map.project(lngLat).x;
101 | const y = this.map.project(lngLat).y;
102 | let translate = {
103 | x: offset,
104 | y: offset,
105 | };
106 | if (y > compassHeight) {
107 | translate.y = y - compassHeight / 2;
108 | }
109 | if (y >= maxHeight - compassHeight / 2) {
110 | translate.y = maxHeight - compassHeight;
111 | }
112 | if (x > compassWidth) {
113 | translate.x = x - compassWidth / 2;
114 | }
115 | if (x >= maxWidth - compassWidth / 2) {
116 | translate.x = maxWidth - compassWidth;
117 | }
118 | compass.style.transform = `translate(${translate.x}px, ${translate.y}px)`;
119 | if (!bounds.contains(lngLat)) {
120 | const angleDeg =
121 | (Math.atan2(translate.y - y, translate.x - x) * 180) / Math.PI;
122 | compass.style.setProperty("--marker-compass-angle", `${angleDeg}deg`);
123 | compass.style.opacity = 1;
124 | compass.style.pointerEvents = "all";
125 | } else {
126 | compass.style.pointerEvents = "none";
127 | compass.style.opacity = 0;
128 | }
129 | });
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import "./MarkerCompass.css";
2 | import { MarkerCompass } from "./MarkerCompass.js";
3 | export { MarkerCompass };
4 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 |
3 | import path from "path";
4 |
5 | export default defineConfig({
6 | server: {
7 | port: 3000,
8 | },
9 | build: {
10 | minify: true,
11 | lib: {
12 | entry: path.resolve(__dirname, "src/main.js"),
13 | name: "mapboxgl-marker-compass",
14 | fileName: "mapboxgl-marker-compass",
15 | },
16 | rollupOptions: {
17 | output: {
18 | assetFileNames: (assetInfo) => {
19 | if (assetInfo.name === "style.css")
20 | return "mapboxgl-marker-compass.css";
21 | return assetInfo.name;
22 | },
23 | },
24 | },
25 | },
26 | });
27 |
--------------------------------------------------------------------------------