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 +4 -4
- data/CHANGELOG.md +16 -0
- data/bin/maplibre-preview +1 -1
- data/lib/maplibre-preview/version.rb +1 -1
- data/lib/maplibre-preview/views/maplibre_layout.slim +101 -3
- data/lib/maplibre-preview/views/maplibre_map.slim +390 -13
- data/spec/maplibre_preview_spec.rb +12 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ab6c314ff00f531f6088b82f2d11b44206fe947f407d512f725ea5efd4df10d1
|
|
4
|
+
data.tar.gz: 2698dbd6a6c458f4c475d71b5dc549bbfde888c96357c6d18c74060b666ffca7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
@@ -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-
|
|
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:
|
|
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 = () =>
|
|
154
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
.then(response => response.json())
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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.
|
|
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-
|
|
11
|
+
date: 2026-05-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|