maplibre-preview 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17ba0e8d475ee1bb1a213790388e255df8eab1f4a3670382c0c77b3e2967f660
4
- data.tar.gz: 4091e3a89317f63690f447167f68c5bdc887d442c33aad1dc418f7ed850a8abc
3
+ metadata.gz: 61c7dd8ecf3753aa43ba5272cc06dab258cbb643824e74336e21e07680795cd6
4
+ data.tar.gz: 6a008af2ffe1b2f3331dc752ab36041767299cc38271f9f9433567f268952198
5
5
  SHA512:
6
- metadata.gz: 43cb047740512a57492d2e8582e704f3b1a45c61bcc7add1fb8845eea75bf66e05e67e1d86f73003ca4a50aad59204857b6dd816789f68ddc0475cc99a0da4a6
7
- data.tar.gz: 8188dbf8a672afd1f42ad70d51315b4741ce1006a2111f37b84ab71c8dfc8353ac34fb8920f72111ddc50ce7bdf038c90fccabcfe66a17d598fa2589bd84ba52
6
+ metadata.gz: c75eede1968d1ddfaa8c388de5c827bf6f8e27f5bcc628938aca1c61f7ccf2896cb70f9a498f6262f352a4c1a211881a87ac09133da2445cd3e6c02f6fa42aac
7
+ data.tar.gz: 435d1192991bacd4730d547eb3f580f99c8d287fabb29e221432a44b309c6b73e25c198f6abea453e0a9ab0ba5d4de6b83adff1fcd4e68cbd73ec03c67e9f22e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.8.0] - 2026-05-14
4
+
5
+ ### Added
6
+ - **Movable overlay windows** - added `OverlayLayoutManager` for shared drag, edge clamping, snapping and persisted positions of movable map UI panels
7
+ - **Window layout reset** - added `Reset window layout` action in Map Settings to clear saved panel positions from local storage and restore defaults
8
+
9
+ ### Changed
10
+ - **Panel layout foundation** - moved Map Settings, Style Controls, Style Parameters, Performance, Elevation Profile and Tile Boundaries to the shared overlay layout manager
11
+ - **Default panel positions** - kept static map controls attached to map edges and placed Map Settings in the top-left default position
12
+
13
+ ### Fixed
14
+ - **Overlay visibility** - fixed managed panel z-index so map settings, filter controls and static MapLibre controls remain visible
15
+ - **Elevation profile frame** - fixed profile window positioning and sizing while dragging near the bottom edge
16
+
17
+ ## [1.7.2] - 2026-05-14
18
+
19
+ ### Changed
20
+ - **Map settings styling** - rounded all control panel corners and styled range sliders to match the dark UI
21
+
22
+ ## [1.7.0] - 2026-05-14
23
+
24
+ ### Added
25
+ - **Style source parameters** - detect `query_params` / `queryParams` from style sources and source metadata
26
+ - **Style parameters panel** - add a bottom-center collapsible panel for passing detected parameters into style, source, tile, data, and metadata URLs
27
+ - **Date/time parameter inputs** - render date/time-like parameters as `datetime-local` fields and send them as epoch seconds
28
+
29
+ ### Changed
30
+ - **Overlay layout** - stack bottom overlays so Style Parameters, Loading, and Elevation Profile panels do not cover each other
31
+ - **Style parameter state** - persist parameter values per style URL and keep applied values in the page query string
32
+
3
33
  ## [1.6.0] - 2026-05-13
4
34
 
5
35
  ### Added
data/bin/maplibre-preview CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'maplibre-preview'
4
+ require_relative '../lib/maplibre-preview'
5
5
  require 'optparse'
6
6
  require 'json'
7
7
  require 'fileutils'
@@ -0,0 +1,415 @@
1
+ class OverlayLayoutManager {
2
+ constructor(options = {}) {
3
+ this.storageKey = options.storageKey || 'maplibre-preview:overlay-layout:v3';
4
+ this.snapThreshold = options.snapThreshold || 32;
5
+ this.mobileBreakpoint = options.mobileBreakpoint || 768;
6
+ this.edgeGap = options.edgeGap || 10;
7
+ this.getReservedBounds = options.getReservedBounds || (() => ({}));
8
+ this.panels = new Map();
9
+ this.state = this.loadState();
10
+ this.drag = null;
11
+ this.zIndex = 1100;
12
+ this.resizeHandler = () => this.refreshBounds();
13
+ window.addEventListener('resize', this.resizeHandler);
14
+ }
15
+
16
+ registerPanel(config) {
17
+ const element = this.resolveElement(config.element);
18
+ if (!element) return null;
19
+
20
+ const id = config.id || element.id;
21
+ if (!id) return null;
22
+
23
+ const existing = this.panels.get(id);
24
+ if (existing) this.unregisterPanel(id);
25
+
26
+ const panel = {
27
+ id,
28
+ element,
29
+ handle: this.resolveHandle(element, config.handleSelector),
30
+ defaultAnchor: config.defaultAnchor || 'left',
31
+ defaultOffset: config.defaultOffset || {x: 0, y: 0},
32
+ snap: config.snap !== false,
33
+ lockSizeOnDrag: config.lockSizeOnDrag === true,
34
+ movable: config.movable !== false,
35
+ cleanup: []
36
+ };
37
+
38
+ element.dataset.overlayPanelId = id;
39
+ element.classList.add('overlay-managed-panel');
40
+ this.panels.set(id, panel);
41
+
42
+ if (panel.movable && panel.handle) {
43
+ this.prepareHandle(panel);
44
+ }
45
+
46
+ this.applyStoredOrDefaultPosition(panel);
47
+ return panel;
48
+ }
49
+
50
+ unregisterPanel(id) {
51
+ const panel = this.panels.get(id);
52
+ if (!panel) return;
53
+
54
+ panel.cleanup.forEach(fn => fn());
55
+ panel.element.classList.remove('overlay-managed-panel', 'overlay-dragging');
56
+ panel.element.removeAttribute('data-overlay-panel-id');
57
+ this.panels.delete(id);
58
+ }
59
+
60
+ movePanelTo(id, anchor) {
61
+ const panel = this.panels.get(id);
62
+ if (!panel) return;
63
+
64
+ const nextState = {mode: 'anchor', anchor, offset: {x: 0, y: 0}};
65
+ this.setPanelState(panel, nextState);
66
+ }
67
+
68
+ resetPanel(id) {
69
+ const panel = this.panels.get(id);
70
+ if (!panel) return;
71
+
72
+ delete this.state[id];
73
+ this.persistState();
74
+ this.applyStoredOrDefaultPosition(panel);
75
+ }
76
+
77
+ resetLayout() {
78
+ this.state = {};
79
+ this.clearStoredState();
80
+ this.panels.forEach(panel => this.applyStoredOrDefaultPosition(panel));
81
+ }
82
+
83
+ refreshPanel(id) {
84
+ const panel = this.panels.get(id);
85
+ if (!panel) return;
86
+ if (this.drag?.panel?.id === id) return;
87
+
88
+ const panelState = this.state[id] || this.defaultState(panel);
89
+ this.applyPanelState(panel, panelState);
90
+ }
91
+
92
+ refreshBounds() {
93
+ if (this.drag) return;
94
+ this.panels.forEach(panel => this.refreshPanel(panel.id));
95
+ }
96
+
97
+ destroy() {
98
+ window.removeEventListener('resize', this.resizeHandler);
99
+ [...this.panels.keys()].forEach(id => this.unregisterPanel(id));
100
+ }
101
+
102
+ resolveElement(elementOrSelector) {
103
+ if (!elementOrSelector) return null;
104
+ return typeof elementOrSelector === 'string'
105
+ ? document.querySelector(elementOrSelector)
106
+ : elementOrSelector;
107
+ }
108
+
109
+ resolveHandle(element, selector) {
110
+ return selector ? element.querySelector(selector) : element;
111
+ }
112
+
113
+ prepareHandle(panel) {
114
+ const pointerDown = event => this.onPointerDown(event, panel);
115
+ const keyDown = event => this.onHandleKeyDown(event, panel);
116
+
117
+ panel.handle.classList.add('overlay-panel-handle');
118
+ panel.handle.tabIndex = panel.handle.tabIndex >= 0 ? panel.handle.tabIndex : 0;
119
+ if (!panel.handle.querySelector('button, input, select, textarea, a')) {
120
+ panel.handle.setAttribute('role', panel.handle.getAttribute('role') || 'button');
121
+ }
122
+ panel.handle.setAttribute('aria-label', panel.handle.getAttribute('aria-label') || `Move ${panel.id} panel`);
123
+ panel.handle.addEventListener('pointerdown', pointerDown);
124
+ panel.handle.addEventListener('keydown', keyDown);
125
+ panel.cleanup.push(() => panel.handle.removeEventListener('pointerdown', pointerDown));
126
+ panel.cleanup.push(() => panel.handle.removeEventListener('keydown', keyDown));
127
+ }
128
+
129
+ onPointerDown(event, panel) {
130
+ if (event.button !== 0 || !panel.movable || this.isInteractiveTarget(event.target)) return;
131
+
132
+ const rect = panel.element.getBoundingClientRect();
133
+ const pointerMove = moveEvent => this.onPointerMove(moveEvent);
134
+ const pointerUp = upEvent => this.onPointerUp(upEvent);
135
+
136
+ if (panel.lockSizeOnDrag) {
137
+ panel.element.style.width = `${Math.round(rect.width)}px`;
138
+ panel.element.style.height = `${Math.round(rect.height)}px`;
139
+ }
140
+
141
+ this.drag = {
142
+ panel,
143
+ pointerId: event.pointerId,
144
+ startX: event.clientX,
145
+ startY: event.clientY,
146
+ originX: rect.left,
147
+ originY: rect.top,
148
+ pointerMove,
149
+ pointerUp
150
+ };
151
+
152
+ panel.element.classList.add('overlay-dragging');
153
+ this.bringToFront(panel);
154
+ try {
155
+ panel.handle.setPointerCapture?.(event.pointerId);
156
+ } catch (e) {
157
+ // Some synthetic pointer events do not create an active pointer capture target.
158
+ }
159
+
160
+ panel.handle.addEventListener('pointermove', pointerMove);
161
+ panel.handle.addEventListener('pointerup', pointerUp);
162
+ panel.handle.addEventListener('pointercancel', pointerUp);
163
+ event.preventDefault();
164
+ }
165
+
166
+ onPointerMove(event) {
167
+ if (!this.drag || event.pointerId !== this.drag.pointerId) return;
168
+
169
+ const nextX = this.drag.originX + event.clientX - this.drag.startX;
170
+ const nextY = this.drag.originY + event.clientY - this.drag.startY;
171
+ const position = this.clampPosition(this.drag.panel, nextX, nextY);
172
+
173
+ this.applyPosition(this.drag.panel, position.x, position.y);
174
+ }
175
+
176
+ onPointerUp(event) {
177
+ if (!this.drag || event.pointerId !== this.drag.pointerId) return;
178
+
179
+ const panel = this.drag.panel;
180
+ const rect = panel.element.getBoundingClientRect();
181
+ const nextState = this.stateFromPosition(panel, rect.left, rect.top);
182
+ this.cleanupDrag();
183
+ this.setPanelState(panel, nextState);
184
+ }
185
+
186
+ cleanupDrag() {
187
+ if (!this.drag) return null;
188
+
189
+ const drag = this.drag;
190
+ try {
191
+ drag.panel.handle.releasePointerCapture?.(drag.pointerId);
192
+ } catch (e) {
193
+ // Pointer capture may already be released by the browser.
194
+ }
195
+ drag.panel.handle.removeEventListener('pointermove', drag.pointerMove);
196
+ drag.panel.handle.removeEventListener('pointerup', drag.pointerUp);
197
+ drag.panel.handle.removeEventListener('pointercancel', drag.pointerUp);
198
+ drag.panel.element.classList.remove('overlay-dragging');
199
+ this.drag = null;
200
+ return drag;
201
+ }
202
+
203
+ onHandleKeyDown(event, panel) {
204
+ if (event.key === 'Escape' && this.drag) {
205
+ this.cleanupDrag();
206
+ this.refreshPanel(panel.id);
207
+ return;
208
+ }
209
+
210
+ if (!event.shiftKey || !['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return;
211
+
212
+ const rect = panel.element.getBoundingClientRect();
213
+ const step = event.altKey ? 32 : 8;
214
+ const delta = {
215
+ ArrowLeft: [-step, 0],
216
+ ArrowRight: [step, 0],
217
+ ArrowUp: [0, -step],
218
+ ArrowDown: [0, step]
219
+ }[event.key];
220
+ const position = this.clampPosition(panel, rect.left + delta[0], rect.top + delta[1]);
221
+
222
+ this.setPanelState(panel, {mode: 'free', x: position.x, y: position.y});
223
+ event.preventDefault();
224
+ }
225
+
226
+ isInteractiveTarget(target) {
227
+ return target?.closest?.('button, input, select, textarea, a, label, [data-overlay-ignore-drag="true"]');
228
+ }
229
+
230
+ bringToFront(panel) {
231
+ this.zIndex += 1;
232
+ panel.element.style.zIndex = String(this.zIndex);
233
+ }
234
+
235
+ loadState() {
236
+ try {
237
+ return JSON.parse(localStorage.getItem(this.storageKey) || '{}');
238
+ } catch (e) {
239
+ console.warn('OverlayLayoutManager: could not load layout state', e);
240
+ return {};
241
+ }
242
+ }
243
+
244
+ persistState() {
245
+ try {
246
+ localStorage.setItem(this.storageKey, JSON.stringify(this.state));
247
+ } catch (e) {
248
+ console.warn('OverlayLayoutManager: could not persist layout state', e);
249
+ }
250
+ }
251
+
252
+ clearStoredState() {
253
+ try {
254
+ localStorage.removeItem(this.storageKey);
255
+ } catch (e) {
256
+ console.warn('OverlayLayoutManager: could not clear layout state', e);
257
+ }
258
+ }
259
+
260
+ defaultState(panel) {
261
+ const offset = typeof panel.defaultOffset === 'function'
262
+ ? panel.defaultOffset(panel)
263
+ : panel.defaultOffset;
264
+
265
+ return {
266
+ mode: 'anchor',
267
+ anchor: panel.defaultAnchor,
268
+ offset: {...offset}
269
+ };
270
+ }
271
+
272
+ applyStoredOrDefaultPosition(panel) {
273
+ const panelState = this.state[panel.id] || this.defaultState(panel);
274
+ this.applyPanelState(panel, panelState);
275
+ }
276
+
277
+ setPanelState(panel, panelState) {
278
+ this.state[panel.id] = panelState;
279
+ this.persistState();
280
+ this.applyPanelState(panel, panelState);
281
+ }
282
+
283
+ applyPanelState(panel, panelState) {
284
+ const responsiveState = this.isConstrainedViewport() && panelState.mode === 'free'
285
+ ? this.defaultState(panel)
286
+ : panelState;
287
+ const position = responsiveState.mode === 'free'
288
+ ? this.clampPosition(panel, responsiveState.x, responsiveState.y)
289
+ : this.positionForAnchor(panel, responsiveState.anchor, responsiveState.offset || {x: 0, y: 0});
290
+
291
+ this.applyPosition(panel, position.x, position.y);
292
+ }
293
+
294
+ applyPosition(panel, x, y) {
295
+ panel.element.style.position = 'fixed';
296
+ panel.element.style.left = `${Math.round(x)}px`;
297
+ panel.element.style.top = `${Math.round(y)}px`;
298
+ panel.element.style.right = 'auto';
299
+ panel.element.style.bottom = 'auto';
300
+ panel.element.style.transform = 'none';
301
+ }
302
+
303
+ positionForAnchor(panel, anchor, offset) {
304
+ const rect = this.panelRect(panel);
305
+ const viewport = this.viewportBounds();
306
+ const centerX = (viewport.width - rect.width) / 2;
307
+ const centerY = (viewport.height - viewport.reservedBottom - rect.height) / 2;
308
+ let x = centerX;
309
+ let y = centerY;
310
+
311
+ if (anchor === 'left') {
312
+ x = this.edgeGap + (offset.x || 0);
313
+ y = centerY + (offset.y || 0);
314
+ } else if (anchor === 'right') {
315
+ x = viewport.width - rect.width - this.edgeGap + (offset.x || 0);
316
+ y = centerY + (offset.y || 0);
317
+ } else if (anchor === 'top') {
318
+ x = centerX + (offset.x || 0);
319
+ y = this.edgeGap + (offset.y || 0);
320
+ } else if (anchor === 'bottom') {
321
+ x = centerX + (offset.x || 0);
322
+ y = viewport.height - viewport.reservedBottom - rect.height - this.edgeGap + (offset.y || 0);
323
+ } else if (anchor === 'top-left') {
324
+ x = this.edgeGap + (offset.x || 0);
325
+ y = this.edgeGap + (offset.y || 0);
326
+ } else if (anchor === 'top-right') {
327
+ x = viewport.width - rect.width - this.edgeGap + (offset.x || 0);
328
+ y = this.edgeGap + (offset.y || 0);
329
+ } else if (anchor === 'bottom-left') {
330
+ x = this.edgeGap + (offset.x || 0);
331
+ y = viewport.height - viewport.reservedBottom - rect.height - this.edgeGap + (offset.y || 0);
332
+ } else if (anchor === 'bottom-right') {
333
+ x = viewport.width - rect.width - this.edgeGap + (offset.x || 0);
334
+ y = viewport.height - viewport.reservedBottom - rect.height - this.edgeGap + (offset.y || 0);
335
+ }
336
+
337
+ return this.clampPosition(panel, x, y);
338
+ }
339
+
340
+ stateFromPosition(panel, x, y) {
341
+ if (this.isConstrainedViewport()) {
342
+ return {mode: 'anchor', anchor: this.nearestAnchor(panel, x, y).anchor, offset: {x: 0, y: 0}};
343
+ }
344
+
345
+ const nearest = this.nearestAnchor(panel, x, y);
346
+ if (!panel.snap || nearest.distance > this.snapThreshold) {
347
+ const clamped = this.clampPosition(panel, x, y);
348
+ return {mode: 'free', x: clamped.x, y: clamped.y};
349
+ }
350
+
351
+ const rect = this.panelRect(panel);
352
+ const viewport = this.viewportBounds();
353
+ const centerX = (viewport.width - rect.width) / 2;
354
+ const centerY = (viewport.height - viewport.reservedBottom - rect.height) / 2;
355
+ const offset = {x: 0, y: 0};
356
+
357
+ if (nearest.anchor === 'left' || nearest.anchor === 'right') {
358
+ offset.y = y - centerY;
359
+ } else {
360
+ offset.x = x - centerX;
361
+ }
362
+
363
+ return {mode: 'anchor', anchor: nearest.anchor, offset};
364
+ }
365
+
366
+ nearestAnchor(panel, x, y) {
367
+ const rect = this.panelRect(panel);
368
+ const viewport = this.viewportBounds();
369
+ const distances = [
370
+ ['left', x],
371
+ ['right', viewport.width - (x + rect.width)],
372
+ ['top', y],
373
+ ['bottom', viewport.height - viewport.reservedBottom - (y + rect.height)]
374
+ ].map(([anchor, distance]) => ({anchor, distance: Math.abs(distance)}));
375
+
376
+ return distances.sort((a, b) => a.distance - b.distance)[0];
377
+ }
378
+
379
+ clampPosition(panel, x, y) {
380
+ const rect = this.panelRect(panel);
381
+ const viewport = this.viewportBounds();
382
+ const minX = this.edgeGap;
383
+ const minY = this.edgeGap;
384
+ const maxX = Math.max(minX, viewport.width - rect.width - this.edgeGap);
385
+ const maxY = Math.max(minY, viewport.height - viewport.reservedBottom - rect.height - this.edgeGap);
386
+
387
+ return {
388
+ x: Math.min(maxX, Math.max(minX, x)),
389
+ y: Math.min(maxY, Math.max(minY, y))
390
+ };
391
+ }
392
+
393
+ panelRect(panel) {
394
+ const rect = panel.element.getBoundingClientRect();
395
+ return {
396
+ width: rect.width || panel.element.offsetWidth || 1,
397
+ height: rect.height || panel.element.offsetHeight || 1
398
+ };
399
+ }
400
+
401
+ viewportBounds() {
402
+ const reserved = this.getReservedBounds() || {};
403
+ return {
404
+ width: window.innerWidth,
405
+ height: window.innerHeight,
406
+ reservedBottom: Math.max(0, Number(reserved.bottom || 0))
407
+ };
408
+ }
409
+
410
+ isConstrainedViewport() {
411
+ return window.innerWidth < this.mobileBreakpoint;
412
+ }
413
+ }
414
+
415
+ window.OverlayLayoutManager = OverlayLayoutManager;
@@ -44,6 +44,13 @@ class TileGridManager {
44
44
  `;
45
45
 
46
46
  document.getElementById('map-container').appendChild(this.panelContainer);
47
+ window.overlayLayoutManager?.registerPanel({
48
+ id: 'tilegrid',
49
+ element: this.panelContainer,
50
+ handleSelector: '.tilegrid-title',
51
+ defaultAnchor: 'right',
52
+ defaultOffset: {x: 0, y: 0}
53
+ });
47
54
  }
48
55
 
49
56
  setupEventListeners() {
@@ -114,6 +121,7 @@ class TileGridManager {
114
121
 
115
122
  if (this.panelContainer) {
116
123
  this.panelContainer.style.display = this.isVisible ? 'block' : 'none';
124
+ this.isVisible && window.overlayLayoutManager?.refreshPanel('tilegrid');
117
125
  }
118
126
 
119
127
  const btn = document.getElementById('tilegrid-mode-btn');
@@ -163,6 +171,7 @@ class TileGridManager {
163
171
  }
164
172
 
165
173
  if (this.panelContainer && this.panelContainer.parentNode) {
174
+ window.overlayLayoutManager?.unregisterPanel('tilegrid');
166
175
  this.panelContainer.parentNode.removeChild(this.panelContainer);
167
176
  }
168
177
 
@@ -1,3 +1,3 @@
1
1
  module MapLibrePreview
2
- VERSION = '1.6.0'
2
+ VERSION = '1.8.0'
3
3
  end