maplibre-preview 1.6.0 → 1.7.2

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: ab6c314ff00f531f6088b82f2d11b44206fe947f407d512f725ea5efd4df10d1
4
+ data.tar.gz: 2698dbd6a6c458f4c475d71b5dc549bbfde888c96357c6d18c74060b666ffca7
5
5
  SHA512:
6
- metadata.gz: 43cb047740512a57492d2e8582e704f3b1a45c61bcc7add1fb8845eea75bf66e05e67e1d86f73003ca4a50aad59204857b6dd816789f68ddc0475cc99a0da4a6
7
- data.tar.gz: 8188dbf8a672afd1f42ad70d51315b4741ce1006a2111f37b84ab71c8dfc8353ac34fb8920f72111ddc50ce7bdf038c90fccabcfe66a17d598fa2589bd84ba52
6
+ metadata.gz: deefd8864adfd9cdd6189f10834bab986c11cfd50e1413eca36f57238ff3d6d3a98eda71bb370e0c14699542abcc9bb8bdde4b271f2693bd802777184539dc9f
7
+ data.tar.gz: 599cdcbc90b314f3bdc21a765fe6817094cec77073843896fca53b77a7e9be90d5a8ccbfe331dc969dd50fd7dcccb3e3e5721095acead56ee958833d13867a6a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.7.2] - 2026-05-14
4
+
5
+ ### Changed
6
+ - **Map settings styling** - rounded all control panel corners and styled range sliders to match the dark UI
7
+
8
+ ## [1.7.0] - 2026-05-14
9
+
10
+ ### Added
11
+ - **Style source parameters** - detect `query_params` / `queryParams` from style sources and source metadata
12
+ - **Style parameters panel** - add a bottom-center collapsible panel for passing detected parameters into style, source, tile, data, and metadata URLs
13
+ - **Date/time parameter inputs** - render date/time-like parameters as `datetime-local` fields and send them as epoch seconds
14
+
15
+ ### Changed
16
+ - **Overlay layout** - stack bottom overlays so Style Parameters, Loading, and Elevation Profile panels do not cover each other
17
+ - **Style parameter state** - persist parameter values per style URL and keep applied values in the page query string
18
+
3
19
  ## [1.6.0] - 2026-05-13
4
20
 
5
21
  ### 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'
@@ -1,3 +1,3 @@
1
1
  module MapLibrePreview
2
- VERSION = '1.6.0'
2
+ VERSION = '1.7.2'
3
3
  end
@@ -196,7 +196,7 @@ html
196
196
  | border-radius: 4px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
197
197
  | }
198
198
  | .control-section-wrapper:not(.collapsed) .control-section {
199
- | border-right: none; border-radius: 4px 0 0 4px;
199
+ | border-radius: 4px;
200
200
  | }
201
201
  | .control-section.collapsed {
202
202
  | gap: 0;
@@ -276,7 +276,105 @@ html
276
276
  | color: #6897bb; font-family: 'Courier New', monospace; font-weight: bold;
277
277
  | }
278
278
  | .setting-range {
279
- | width: 100%; accent-color: #6897bb; cursor: pointer;
279
+ | width: 100%; height: 18px; margin: 0; accent-color: #6897bb; cursor: pointer;
280
+ | background: transparent; appearance: none; -webkit-appearance: none;
281
+ | }
282
+ | .setting-range:focus {
283
+ | outline: none;
284
+ | }
285
+ | .setting-range:focus-visible::-webkit-slider-thumb {
286
+ | box-shadow: 0 0 0 2px #313335, 0 0 0 4px #6897bb;
287
+ | }
288
+ | .setting-range:focus-visible::-moz-range-thumb {
289
+ | box-shadow: 0 0 0 2px #313335, 0 0 0 4px #6897bb;
290
+ | }
291
+ | .setting-range::-webkit-slider-runnable-track {
292
+ | height: 6px; background: #313335; border: 1px solid #555555; border-radius: 3px;
293
+ | }
294
+ | .setting-range::-moz-range-track {
295
+ | height: 6px; background: #313335; border: 1px solid #555555; border-radius: 3px;
296
+ | }
297
+ | .setting-range::-moz-range-progress {
298
+ | height: 6px; background: #6897bb; border-radius: 3px;
299
+ | }
300
+ | .setting-range::-webkit-slider-thumb {
301
+ | width: 14px; height: 14px; margin-top: -5px; background: #6897bb;
302
+ | border: 1px solid #7aa8cc; border-radius: 50%; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
303
+ | -webkit-appearance: none; transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
304
+ | }
305
+ | .setting-range::-moz-range-thumb {
306
+ | width: 14px; height: 14px; background: #6897bb;
307
+ | border: 1px solid #7aa8cc; border-radius: 50%; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
308
+ | transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
309
+ | }
310
+ | .setting-range:hover::-webkit-slider-runnable-track {
311
+ | border-color: #666666;
312
+ | }
313
+ | .setting-range:hover::-moz-range-track {
314
+ | border-color: #666666;
315
+ | }
316
+ | .setting-range:hover::-webkit-slider-thumb {
317
+ | background: #7aa8cc; border-color: #8bb9dd;
318
+ | }
319
+ | .setting-range:hover::-moz-range-thumb {
320
+ | background: #7aa8cc; border-color: #8bb9dd;
321
+ | }
322
+ | .style-parameter-fields {
323
+ | display: flex; flex-direction: column; gap: 8px;
324
+ | }
325
+ | .style-parameters-overlay {
326
+ | position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%);
327
+ | z-index: 1000; width: min(520px, calc(100vw - 32px));
328
+ | background: rgba(60, 63, 65, 0.95); border: 1px solid #555555;
329
+ | border-radius: 4px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
330
+ | color: #a9b7c6; overflow: hidden;
331
+ | }
332
+ | .style-parameters-header {
333
+ | display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 12px;
334
+ | width: 100%; background: transparent; color: inherit; border: none;
335
+ | padding: 10px 12px; cursor: pointer; text-align: left;
336
+ | }
337
+ | .style-parameters-header:hover {
338
+ | background: rgba(75, 77, 79, 0.95);
339
+ | }
340
+ | .style-parameters-title {
341
+ | color: #ffc66d; font-size: 11px; font-weight: bold; line-height: 1.2;
342
+ | text-transform: uppercase;
343
+ | }
344
+ | .style-parameters-summary {
345
+ | display: inline-flex; align-items: center; gap: 4px; color: #6897bb;
346
+ | font-family: 'Courier New', monospace; font-size: 11px; font-weight: bold;
347
+ | }
348
+ | .style-parameters-toggle-icon {
349
+ | color: #a9b7c6; font-size: 12px; font-weight: bold; line-height: 1;
350
+ | }
351
+ | .style-parameters-overlay:not(.collapsed) .style-parameters-toggle-icon {
352
+ | transform: rotate(180deg);
353
+ | }
354
+ | .style-parameters-body {
355
+ | display: flex; flex-direction: column; gap: 10px;
356
+ | max-height: min(48vh, 420px); overflow-y: auto;
357
+ | padding: 0 12px 12px; border-top: 1px solid #555555;
358
+ | }
359
+ | .style-parameters-overlay.collapsed .style-parameters-body {
360
+ | display: none;
361
+ | }
362
+ | .style-parameter-row {
363
+ | display: flex; flex-direction: column; gap: 4px;
364
+ | }
365
+ | .style-parameter-label {
366
+ | color: #808080; font-size: 10px; font-weight: bold;
367
+ | text-transform: uppercase;
368
+ | }
369
+ | .style-parameter-input {
370
+ | width: 100%; background: #313335; color: #a9b7c6; border: 1px solid #555555;
371
+ | border-radius: 3px; padding: 6px 8px; font-size: 11px; box-sizing: border-box;
372
+ | }
373
+ | .style-parameter-input:focus {
374
+ | outline: none; border-color: #6897bb; box-shadow: 0 0 0 1px #6897bb;
375
+ | }
376
+ | .style-parameter-actions {
377
+ | display: grid; grid-template-columns: 1fr 1fr; gap: 6px;
280
378
  | }
281
379
  | .style-panel {
282
380
  | flex: 1; min-height: 0; max-height: calc(80vh - 245px); width: max-content; max-width: 100%;
@@ -376,7 +474,7 @@ html
376
474
  | background: rgba(60, 63, 65, 0.95); border: 1px solid #555555;
377
475
  | border-radius: 6px; padding: 12px 20px; z-index: 1000;
378
476
  | backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
379
- | transition: all 0.3s ease; display: none;
477
+ | transition: opacity 0.3s ease; display: none;
380
478
  | }
381
479
  | .loading-progress { text-align: center; color: #a9b7c6; }
382
480
  | .loading-text { font-size: 14px; color: #a9b7c6; margin-bottom: 8px; }
@@ -77,6 +77,19 @@ ruby:
77
77
  #map-container
78
78
  #map.map-layer data-style-url="#{style_url}"
79
79
 
80
+ #style-parameters-panel.style-parameters-overlay.collapsed style="display: none;"
81
+ button.style-parameters-header type="button" onclick="toggleStyleParametersPanel()" id="style-parameters-toggle" aria-expanded="false"
82
+ span.style-parameters-title Style parameters
83
+ span.style-parameters-summary
84
+ span#style-parameters-count 0
85
+ span params
86
+ span.style-parameters-toggle-icon ▲
87
+ .style-parameters-body
88
+ #style-parameter-fields.style-parameter-fields
89
+ .style-parameter-actions
90
+ button.control-button type="button" onclick="applyStyleParameters()" id="style-parameters-apply" Apply
91
+ button.control-button type="button" onclick="resetStyleParameters()" id="style-parameters-reset" Reset
92
+
80
93
  #version-info.version-info
81
94
  a.version-info-version href="https://github.com/artyomb/maplibre-preview" target="_blank" v#{MapLibrePreview::VERSION}
82
95
 
@@ -137,10 +150,277 @@ javascript:
137
150
  let currentProfile = null;
138
151
  let contourManager = null;
139
152
  let tileGridManager = null;
153
+ let originalStyle = null;
154
+ let styleParameterDefinitions = new Map();
155
+ let styleParameterValues = {};
156
+ let parameterizedUrlPrefixes = new Set();
140
157
 
141
158
  const toDomId = (prefix, id) => `${prefix}-${String(id).replace(/[^a-zA-Z0-9_-]/g, '_')}`;
142
159
  const noCacheRequestOptions = () => mapCacheDisabled ? {cache: 'no-store'} : undefined;
143
160
  const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
161
+ const PAGE_STYLE_PARAMS = new Set(['style', 'style_url']);
162
+
163
+ const normalizeQueryParams = (value) => {
164
+ if (Array.isArray(value)) return value.map(String).filter(Boolean);
165
+ if (typeof value === 'string') return value.split(',').map(item => item.trim()).filter(Boolean);
166
+ return [];
167
+ };
168
+
169
+ const inferParameterInputType = (name) => /(^|_|-)(time|date|datetime)($|_|-)/i.test(name) ? 'datetime-local' : 'text';
170
+
171
+ const parameterInputToQueryValue = (name, value) => {
172
+ if (!value) return '';
173
+ if (inferParameterInputType(name) !== 'datetime-local') return value;
174
+
175
+ const parsed = new Date(value);
176
+ return Number.isNaN(parsed.getTime()) ? value : String(Math.floor(parsed.getTime() / 1000));
177
+ };
178
+
179
+ const queryValueToParameterInput = (name, value) => {
180
+ if (!value || inferParameterInputType(name) !== 'datetime-local') return value || '';
181
+
182
+ const parsed = /^\d+$/.test(String(value)) ? new Date(Number(value) * 1000) : new Date(value);
183
+ if (Number.isNaN(parsed.getTime())) return '';
184
+
185
+ const pad = number => String(number).padStart(2, '0');
186
+ return `${parsed.getFullYear()}-${pad(parsed.getMonth() + 1)}-${pad(parsed.getDate())}T${pad(parsed.getHours())}:${pad(parsed.getMinutes())}`;
187
+ };
188
+
189
+ const styleParameterStorageKey = (name) => `maplibre-preview:style-parameter:${style_url || 'default'}:${name}`;
190
+
191
+ const mergeStyleParameterDefinition = (name, sourceName = null) => {
192
+ if (!name || PAGE_STYLE_PARAMS.has(name)) return;
193
+
194
+ const current = styleParameterDefinitions.get(name) || {name, sources: new Set()};
195
+ sourceName && current.sources.add(sourceName);
196
+ styleParameterDefinitions.set(name, current);
197
+ };
198
+
199
+ const sourceDeclaredParameters = (sourceDef) => [
200
+ ...normalizeQueryParams(sourceDef?.query_params),
201
+ ...normalizeQueryParams(sourceDef?.queryParams)
202
+ ];
203
+
204
+ const urlPrefixFromTemplate = (template) => {
205
+ if (!template || typeof template !== 'string') return null;
206
+ const tokenIndex = template.search(/\{[^}]+\}/);
207
+ const prefix = tokenIndex >= 0 ? template.slice(0, tokenIndex) : template;
208
+ return prefix ? new URL(prefix, window.location.href).toString() : null;
209
+ };
210
+
211
+ const rememberParameterizedUrl = (template) => {
212
+ const prefix = urlPrefixFromTemplate(template);
213
+ prefix && parameterizedUrlPrefixes.add(prefix);
214
+ };
215
+
216
+ const appendStyleParametersToUrl = (resourceUrl, values = styleParameterValues, parameterNames = Object.keys(values)) => {
217
+ if (!resourceUrl || !parameterNames.length) return resourceUrl;
218
+
219
+ const tokens = [];
220
+ const tokenizedUrl = String(resourceUrl).replace(/\{[^}]+\}/g, match => {
221
+ const placeholder = `__MLP_TOKEN_${tokens.length}__`;
222
+ tokens.push([placeholder, match]);
223
+ return placeholder;
224
+ });
225
+ const url = new URL(tokenizedUrl, window.location.href);
226
+ parameterNames.forEach(name => {
227
+ const value = values[name];
228
+ value === undefined || value === null || value === ''
229
+ ? url.searchParams.delete(name)
230
+ : url.searchParams.set(name, String(value));
231
+ });
232
+ return tokens.reduce((result, [placeholder, token]) => result.replace(placeholder, token), url.toString());
233
+ };
234
+
235
+ const collectStyleSourceParameters = (style) => {
236
+ Object.entries(style?.sources || {}).forEach(([sourceName, sourceDef]) => {
237
+ if (sourceName === 'preview-basemap') return;
238
+
239
+ const params = sourceDeclaredParameters(sourceDef);
240
+ params.forEach(name => mergeStyleParameterDefinition(name, sourceName));
241
+
242
+ if (params.length) {
243
+ [sourceDef.url, sourceDef.meta_url, sourceDef.data].forEach(rememberParameterizedUrl);
244
+ (sourceDef.tiles || []).forEach(rememberParameterizedUrl);
245
+ }
246
+ });
247
+ };
248
+
249
+ const fetchSourceMetadata = async (url) => {
250
+ try {
251
+ const response = await fetch(appendStyleParametersToUrl(url), noCacheRequestOptions());
252
+ return response.ok ? response.json() : null;
253
+ } catch (e) {
254
+ console.warn('Could not inspect source metadata:', url, e);
255
+ return null;
256
+ }
257
+ };
258
+
259
+ const collectSourceMetadataParameters = async (style) => {
260
+ const entries = Object.entries(style?.sources || {}).filter(([, sourceDef]) => sourceDef?.url || sourceDef?.meta_url);
261
+
262
+ await Promise.all(entries.map(async ([sourceName, sourceDef]) => {
263
+ const metadataUrls = [sourceDef.meta_url];
264
+ if (sourceDef.type !== 'image') metadataUrls.push(sourceDef.url);
265
+
266
+ for (const metadataUrl of metadataUrls.filter(Boolean)) {
267
+ const metadata = await fetchSourceMetadata(metadataUrl);
268
+ if (!metadata) continue;
269
+
270
+ const params = normalizeQueryParams(metadata.query_params || metadata.queryParams);
271
+ params.forEach(name => mergeStyleParameterDefinition(name, sourceName));
272
+ (metadata.tiles || []).forEach(rememberParameterizedUrl);
273
+ }
274
+ }));
275
+ };
276
+
277
+ const getInitialStyleParameterValue = (name) => {
278
+ const pageParams = new URLSearchParams(window.location.search);
279
+ const urlValue = pageParams.get(name);
280
+ if (urlValue !== null) return urlValue;
281
+
282
+ try {
283
+ return localStorage.getItem(styleParameterStorageKey(name)) || '';
284
+ } catch (e) {
285
+ return '';
286
+ }
287
+ };
288
+
289
+ const pageProvidedStyleParameterValues = () => {
290
+ const values = {};
291
+ new URLSearchParams(window.location.search).forEach((value, key) => {
292
+ if (!PAGE_STYLE_PARAMS.has(key)) values[key] = value;
293
+ });
294
+ return values;
295
+ };
296
+
297
+ const renderStyleParameterControls = () => {
298
+ const panel = document.getElementById('style-parameters-panel');
299
+ const fields = document.getElementById('style-parameter-fields');
300
+ const count = document.getElementById('style-parameters-count');
301
+ if (!panel || !fields) return;
302
+
303
+ const definitions = [...styleParameterDefinitions.values()].sort((a, b) => a.name.localeCompare(b.name));
304
+ panel.style.display = definitions.length ? 'block' : 'none';
305
+ count && (count.textContent = String(definitions.length));
306
+ fields.innerHTML = '';
307
+
308
+ definitions.forEach(definition => {
309
+ const row = document.createElement('label');
310
+ row.className = 'style-parameter-row';
311
+ row.htmlFor = `style-param-${definition.name}`;
312
+
313
+ const label = document.createElement('span');
314
+ label.className = 'style-parameter-label';
315
+ label.textContent = definition.name;
316
+
317
+ const input = document.createElement('input');
318
+ input.className = 'style-parameter-input';
319
+ input.id = `style-param-${definition.name}`;
320
+ input.type = inferParameterInputType(definition.name);
321
+ input.value = queryValueToParameterInput(definition.name, styleParameterValues[definition.name]);
322
+ input.dataset.parameterName = definition.name;
323
+ input.title = [...definition.sources].join(', ');
324
+
325
+ row.appendChild(label);
326
+ row.appendChild(input);
327
+ fields.appendChild(row);
328
+ });
329
+
330
+ layoutBottomOverlays();
331
+ };
332
+
333
+ const toggleStyleParametersPanel = () => {
334
+ const panel = document.getElementById('style-parameters-panel');
335
+ const toggle = document.getElementById('style-parameters-toggle');
336
+ if (!panel) return;
337
+
338
+ const isCollapsed = panel.classList.toggle('collapsed');
339
+ toggle?.setAttribute('aria-expanded', String(!isCollapsed));
340
+ layoutBottomOverlays();
341
+ };
342
+
343
+ const initializeStyleParameters = async (style) => {
344
+ styleParameterDefinitions = new Map();
345
+ styleParameterValues = {};
346
+ parameterizedUrlPrefixes = new Set();
347
+
348
+ collectStyleSourceParameters(style);
349
+ styleParameterDefinitions.forEach((definition, name) => {
350
+ styleParameterValues[name] = getInitialStyleParameterValue(name);
351
+ });
352
+ await collectSourceMetadataParameters(style);
353
+
354
+ styleParameterDefinitions.forEach((definition, name) => {
355
+ styleParameterValues[name] ??= getInitialStyleParameterValue(name);
356
+ });
357
+
358
+ renderStyleParameterControls();
359
+ };
360
+
361
+ const getSourceParameterNames = (sourceName, sourceDef) => {
362
+ const names = new Set(sourceDeclaredParameters(sourceDef));
363
+ styleParameterDefinitions.forEach((definition, name) => {
364
+ definition.sources.has(sourceName) && names.add(name);
365
+ });
366
+ return [...names];
367
+ };
368
+
369
+ const applyStyleParametersToStyle = (style) => {
370
+ const modifiedStyle = JSON.parse(JSON.stringify(style));
371
+
372
+ Object.entries(modifiedStyle.sources || {}).forEach(([sourceName, sourceDef]) => {
373
+ if (sourceName === 'preview-basemap') return;
374
+
375
+ const parameterNames = getSourceParameterNames(sourceName, sourceDef);
376
+ if (!parameterNames.length) return;
377
+
378
+ typeof sourceDef.url === 'string' && (sourceDef.url = appendStyleParametersToUrl(sourceDef.url, styleParameterValues, parameterNames));
379
+ typeof sourceDef.meta_url === 'string' && (sourceDef.meta_url = appendStyleParametersToUrl(sourceDef.meta_url, styleParameterValues, parameterNames));
380
+ typeof sourceDef.data === 'string' && (sourceDef.data = appendStyleParametersToUrl(sourceDef.data, styleParameterValues, parameterNames));
381
+ Array.isArray(sourceDef.tiles) && (sourceDef.tiles = sourceDef.tiles.map(tileUrl => appendStyleParametersToUrl(tileUrl, styleParameterValues, parameterNames)));
382
+ });
383
+
384
+ return modifiedStyle;
385
+ };
386
+
387
+ const shouldPatchRequestUrl = (resourceUrl) => {
388
+ if (!Object.values(styleParameterValues).some(value => value !== undefined && value !== null && value !== '')) return false;
389
+ const absolute = new URL(resourceUrl, window.location.href).toString();
390
+ return absolute.includes('/rb_tiles/') || [...parameterizedUrlPrefixes].some(prefix => absolute.startsWith(prefix));
391
+ };
392
+
393
+ const patchRequestUrl = (resourceUrl) => shouldPatchRequestUrl(resourceUrl) ? appendStyleParametersToUrl(resourceUrl) : resourceUrl;
394
+
395
+ const BOTTOM_OVERLAY_IDS = ['style-parameters-panel', 'loading-indicator', 'profile-overlay'];
396
+ const BOTTOM_OVERLAY_BASE_OFFSET = 20;
397
+ const BOTTOM_OVERLAY_GAP = 12;
398
+
399
+ const isVisibleBottomOverlay = (element) => {
400
+ if (!element) return false;
401
+
402
+ const style = window.getComputedStyle(element);
403
+ return style.display !== 'none' && style.visibility !== 'hidden' && element.getClientRects().length > 0;
404
+ };
405
+
406
+ const layoutBottomOverlays = () => {
407
+ window.requestAnimationFrame(() => {
408
+ let bottomOffset = BOTTOM_OVERLAY_BASE_OFFSET;
409
+
410
+ BOTTOM_OVERLAY_IDS.forEach(id => {
411
+ const element = document.getElementById(id);
412
+ if (!element) return;
413
+
414
+ if (!isVisibleBottomOverlay(element)) {
415
+ element.style.bottom = '';
416
+ return;
417
+ }
418
+
419
+ element.style.bottom = `${bottomOffset}px`;
420
+ bottomOffset += element.offsetHeight + BOTTOM_OVERLAY_GAP;
421
+ });
422
+ });
423
+ };
144
424
 
145
425
  const getRealTerrainElevation = (lngLat) => {
146
426
  const elevation = map.queryTerrainElevation(lngLat);
@@ -150,8 +430,15 @@ javascript:
150
430
  return elevation / exaggeration;
151
431
  };
152
432
 
153
- const showLoading = () => document.getElementById('loading-indicator').style.display = 'block';
154
- const hideLoading = () => document.getElementById('loading-indicator').style.display = 'none';
433
+ const showLoading = () => {
434
+ document.getElementById('loading-indicator').style.display = 'block';
435
+ layoutBottomOverlays();
436
+ };
437
+
438
+ const hideLoading = () => {
439
+ document.getElementById('loading-indicator').style.display = 'none';
440
+ layoutBottomOverlays();
441
+ };
155
442
 
156
443
  const updateLoadingProgress = () => {
157
444
  let progress = 0;
@@ -256,7 +543,7 @@ javascript:
256
543
  canvasContextAttributes: {antialias: antialiasEnabled},
257
544
  fadeDuration: tileFadeEnabled ? 300 : 0,
258
545
  transformRequest: (url) => ({
259
- url,
546
+ url: patchRequestUrl(url),
260
547
  ...noCacheRequestOptions()
261
548
  })
262
549
  });
@@ -306,19 +593,21 @@ javascript:
306
593
  popup.setLngLat(e.lngLat).setHTML(tooltips.join('<br>')).addTo(map);
307
594
  };
308
595
 
309
- const initializeMap = () => {
596
+ const initializeMap = async () => {
310
597
  showLoading();
311
598
  const emptyStyle = {version: 8, sources: {}, layers: []};
312
599
 
313
- style_url
314
- ? fetch(style_url, noCacheRequestOptions())
315
- .then(response => response.json())
316
- .then(originalStyle => createMapWithStyle(addBasemapToStyle(originalStyle)))
317
- .catch(error => {
318
- console.error('Style loading error:', error);
319
- createMapWithStyle(addBasemapToStyle(emptyStyle));
320
- })
321
- : createMapWithStyle(addBasemapToStyle(emptyStyle));
600
+ try {
601
+ originalStyle = style_url
602
+ ? await fetch(appendStyleParametersToUrl(style_url, pageProvidedStyleParameterValues(), Object.keys(pageProvidedStyleParameterValues())), noCacheRequestOptions()).then(response => response.json())
603
+ : emptyStyle;
604
+ } catch (error) {
605
+ console.error('Style loading error:', error);
606
+ originalStyle = emptyStyle;
607
+ }
608
+
609
+ await initializeStyleParameters(originalStyle);
610
+ createMapWithStyle(addBasemapToStyle(applyStyleParametersToStyle(originalStyle)));
322
611
  };
323
612
 
324
613
  const createMapWithStyle = (style) => {
@@ -659,6 +948,7 @@ javascript:
659
948
  [header, stats, chart].forEach(el => overlay.appendChild(el));
660
949
  document.getElementById('map-container').appendChild(overlay);
661
950
  setTimeout(() => drawSimpleProfileChart(profile), 10);
951
+ layoutBottomOverlays();
662
952
  };
663
953
 
664
954
  const drawSimpleProfileChart = (profile) => {
@@ -736,6 +1026,7 @@ javascript:
736
1026
 
737
1027
  const hideProfile = () => {
738
1028
  document.getElementById('profile-overlay')?.remove();
1029
+ layoutBottomOverlays();
739
1030
  map.getLayer('profile-line') && (map.removeLayer('profile-line'), map.removeSource('profile-line'));
740
1031
  map.getLayer('temporary-line') && (map.removeLayer('temporary-line'), map.removeSource('temporary-line'));
741
1032
  hideMapMarker();
@@ -1078,6 +1369,88 @@ javascript:
1078
1369
  updateTileFadeButton();
1079
1370
  };
1080
1371
 
1372
+ const syncStyleParameterUrlState = () => {
1373
+ const url = new URL(window.location.href);
1374
+ Object.entries(styleParameterValues).forEach(([name, value]) => {
1375
+ value === undefined || value === null || value === ''
1376
+ ? url.searchParams.delete(name)
1377
+ : url.searchParams.set(name, String(value));
1378
+ });
1379
+ window.history.replaceState({}, '', url.toString());
1380
+ };
1381
+
1382
+ const readStyleParameterFormValues = () => {
1383
+ document.querySelectorAll('.style-parameter-input').forEach(input => {
1384
+ const name = input.dataset.parameterName;
1385
+ if (!name) return;
1386
+ styleParameterValues[name] = parameterInputToQueryValue(name, input.value);
1387
+
1388
+ try {
1389
+ styleParameterValues[name]
1390
+ ? localStorage.setItem(styleParameterStorageKey(name), styleParameterValues[name])
1391
+ : localStorage.removeItem(styleParameterStorageKey(name));
1392
+ } catch (e) {
1393
+ console.warn('Could not persist style parameter:', name, e);
1394
+ }
1395
+ });
1396
+ };
1397
+
1398
+ const resetRuntimeStateBeforeReload = () => {
1399
+ stopPerformanceMonitoring();
1400
+ tileGridManager?.cleanup();
1401
+ contourManager?.cleanup();
1402
+ document.getElementById('filter-buttons') && (document.getElementById('filter-buttons').innerHTML = '');
1403
+ document.getElementById('layer-buttons') && (document.getElementById('layer-buttons').innerHTML = '');
1404
+ Object.keys(layerStates).forEach(key => delete layerStates[key]);
1405
+ Object.keys(layerIdToDomId).forEach(key => delete layerIdToDomId[key]);
1406
+ filters = null;
1407
+ contourManager = null;
1408
+ tileGridManager = null;
1409
+ window.tileGridManager = null;
1410
+ currentStyle = null;
1411
+ styleLoaded = false;
1412
+ resourcesLoaded = 0;
1413
+ totalResources = 0;
1414
+ tilesLoaded = 0;
1415
+ tilesTotal = 0;
1416
+ profilePoints = [];
1417
+ currentProfile = null;
1418
+ profileLine = null;
1419
+ hideProfile();
1420
+ hideElevationTooltip();
1421
+ };
1422
+
1423
+ const reloadStyleWithParameters = () => {
1424
+ if (!originalStyle) return;
1425
+
1426
+ showLoading();
1427
+ resetRuntimeStateBeforeReload();
1428
+ map?.remove();
1429
+ map = null;
1430
+ createMapWithStyle(addBasemapToStyle(applyStyleParametersToStyle(originalStyle)));
1431
+ };
1432
+
1433
+ const applyStyleParameters = () => {
1434
+ readStyleParameterFormValues();
1435
+ syncStyleParameterUrlState();
1436
+ renderStyleParameterControls();
1437
+ reloadStyleWithParameters();
1438
+ };
1439
+
1440
+ const resetStyleParameters = () => {
1441
+ Object.keys(styleParameterValues).forEach(name => {
1442
+ styleParameterValues[name] = '';
1443
+ try {
1444
+ localStorage.removeItem(styleParameterStorageKey(name));
1445
+ } catch (e) {
1446
+ console.warn('Could not clear style parameter:', name, e);
1447
+ }
1448
+ });
1449
+ syncStyleParameterUrlState();
1450
+ renderStyleParameterControls();
1451
+ reloadStyleWithParameters();
1452
+ };
1453
+
1081
1454
  const toggleControlSection = (sectionName) => {
1082
1455
  const wrapper = document.getElementById(`${sectionName}-wrapper`);
1083
1456
  const section = document.getElementById(`${sectionName}-section`);
@@ -1108,10 +1481,14 @@ javascript:
1108
1481
  window.toggleCollisionBoxes = toggleCollisionBoxes;
1109
1482
  window.toggleOverdrawInspector = toggleOverdrawInspector;
1110
1483
  window.toggleTileFade = toggleTileFade;
1484
+ window.applyStyleParameters = applyStyleParameters;
1485
+ window.resetStyleParameters = resetStyleParameters;
1486
+ window.toggleStyleParametersPanel = toggleStyleParametersPanel;
1111
1487
  window.toggleControlSection = toggleControlSection;
1112
1488
  window.toggleLayerControls = () => toggleControlSection('style-controls');
1113
1489
  window.hideProfile = hideProfile;
1114
1490
  window.toggleTileGrid = toggleTileGrid;
1115
1491
  window.tileGridManager = null;
1492
+ window.addEventListener('resize', layoutBottomOverlays);
1116
1493
 
1117
1494
  initializeMap();
@@ -33,9 +33,19 @@ RSpec.describe MapLibrePreview do
33
33
  expect(last_response.body).to include('id="collision-boxes-btn"')
34
34
  expect(last_response.body).to include('id="overdraw-inspector-btn"')
35
35
  expect(last_response.body).to include('id="tile-fade-btn"')
36
+ expect(last_response.body).to include('id="style-parameters-panel"')
37
+ expect(last_response.body).to include('id="style-parameters-toggle"')
38
+ expect(last_response.body).to include('id="style-parameter-fields"')
39
+ expect(last_response.body).to include('id="style-parameters-apply"')
40
+ expect(last_response.body).to include('id="style-parameters-reset"')
36
41
  expect(last_response.body).to include('mapCacheDisabled')
37
42
  expect(last_response.body).to include('basemapOpacity')
38
43
  expect(last_response.body).to include('terrainExaggeration')
44
+ expect(last_response.body).to include('styleParameterDefinitions')
45
+ expect(last_response.body).to include('sourceDeclaredParameters')
46
+ expect(last_response.body).to include('collectSourceMetadataParameters')
47
+ expect(last_response.body).to include('applyStyleParametersToStyle')
48
+ expect(last_response.body).to include('layoutBottomOverlays')
39
49
  expect(last_response.body).to include('showCollisionBoxes')
40
50
  expect(last_response.body).to include('showOverdrawInspector')
41
51
  expect(last_response.body).to include("cache: 'no-store'")
@@ -45,6 +55,8 @@ RSpec.describe MapLibrePreview do
45
55
  expect(last_response.body).to include('raster-fade-duration')
46
56
  expect(last_response.body).to include('window.toggleMapCache')
47
57
  expect(last_response.body).to include('window.switchSettingsMode')
58
+ expect(last_response.body).to include('window.applyStyleParameters')
59
+ expect(last_response.body).to include('window.toggleStyleParametersPanel')
48
60
  end
49
61
 
50
62
  it 'serves all required JavaScript modules' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: maplibre-preview
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Ludov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-13 00:00:00.000000000 Z
11
+ date: 2026-05-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack