maplibre-preview 1.4.3 → 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.
@@ -12,33 +12,84 @@ ruby:
12
12
  - if style_name || external_style_url || options[:style_url]
13
13
  .layer-controls-wrapper
14
14
  .layer-controls id="layer-controls"
15
- .mode-switcher
16
- button.mode-button.active id="mode-filters" onclick="switchMode(this, 'filters')" Filters
17
- button.mode-button id="mode-layers" onclick="switchMode(this, 'layers')" Layers
18
- button.control-button onclick="toggleHoverMode()" id="hover-mode-btn" Hover Mode
19
- button.control-button onclick="toggleProfileMode()" id="profile-mode-btn" style="display: none;" Elevation Profile
20
- button.control-button onclick="toggleAntialias()" id="antialias-btn" Antialias
21
- button.control-button onclick="toggleTileGrid()" id="tilegrid-mode-btn" Tile Grid
22
-
23
- #filters-panel.control-panel.active
24
- .control-panel-header
25
- button.control-button onclick="toggleAllFilters()" Toggle All Filters
26
- button.control-button onclick="toggleBasemap()" Show/Hide Basemap
27
- .control-panel-content
28
- #filter-buttons
29
-
30
- #layers-panel.control-panel
31
- .control-panel-header
32
- button.control-button onclick="toggleAllLayers()" Toggle All Layers
33
- button.control-button onclick="toggleBasemap()" Show/Hide Basemap
34
- .control-panel-content
35
- #layer-buttons
36
- button.layer-controls-toggle id="layer-controls-toggle" onclick="toggleLayerControls()" title="Collapse/Expand Panel"
37
- span.toggle-icon
15
+ .control-section-wrapper.map-settings-wrapper id="map-settings-wrapper"
16
+ .control-section.map-settings-section id="map-settings-section"
17
+ .control-section-header
18
+ .control-section-title Map Settings
19
+ button.control-section-toggle-inline id="map-settings-toggle-inline" onclick="toggleControlSection('map-settings')" title="Expand Map Settings"
20
+ span.toggle-icon
21
+ .control-section-body
22
+ .mode-switcher id="settings-mode-switcher"
23
+ button.mode-button.active id="settings-mode-view" onclick="switchSettingsMode(this, 'view')" View
24
+ button.mode-button id="settings-mode-debug" onclick="switchSettingsMode(this, 'debug')" Debug
25
+
26
+ #settings-view-panel.control-panel.settings-panel.active
27
+ .setting-group
28
+ .setting-label
29
+ span Basemap opacity
30
+ span.setting-value id="basemap-opacity-value" 80%
31
+ input.setting-range id="basemap-opacity-slider" type="range" min="0" max="100" step="5" value="80" oninput="setBasemapOpacity(this.value)"
32
+ .setting-group.terrain-setting id="terrain-exaggeration-setting" style="display: none;"
33
+ .setting-label
34
+ span Terrain exaggeration
35
+ span.setting-value id="terrain-exaggeration-value" 1.0x
36
+ input.setting-range id="terrain-exaggeration-slider" type="range" min="0" max="3" step="0.1" value="1" oninput="setTerrainExaggeration(this.value)"
37
+ button.control-button onclick="toggleAntialias()" id="antialias-btn" Antialias
38
+
39
+ #settings-debug-panel.control-panel.settings-panel
40
+ button.control-button onclick="toggleMapCache()" id="map-cache-btn" Cache
41
+ button.control-button onclick="toggleTileGrid()" id="tilegrid-mode-btn" Tile Boundaries
42
+ button.control-button onclick="toggleCollisionBoxes()" id="collision-boxes-btn" Collision Boxes
43
+ button.control-button onclick="toggleOverdrawInspector()" id="overdraw-inspector-btn" Overdraw
44
+ button.control-button onclick="toggleTileFade()" id="tile-fade-btn" Raster Fade
45
+ button.control-button onclick="toggleHoverMode()" id="hover-mode-btn" Hover Mode
46
+ button.control-button onclick="toggleProfileMode()" id="profile-mode-btn" style="display: none;" Elevation Profile
47
+ button.control-section-toggle-external id="map-settings-toggle" onclick="toggleControlSection('map-settings')" title="Collapse Map Settings"
48
+ span.toggle-icon ◀
49
+
50
+ .control-section-wrapper.style-controls-wrapper id="style-controls-wrapper"
51
+ .control-section.style-controls-section id="style-controls-section"
52
+ .control-section-header
53
+ .control-section-title Style Controls
54
+ button.control-section-toggle-inline id="style-controls-toggle-inline" onclick="toggleControlSection('style-controls')" title="Expand Style Controls"
55
+ span.toggle-icon ▶
56
+ .control-section-body
57
+ .mode-switcher id="style-mode-switcher"
58
+ button.mode-button.active id="mode-filters" onclick="switchMode(this, 'filters')" Filters
59
+ button.mode-button id="mode-layers" onclick="switchMode(this, 'layers')" Layers
60
+
61
+ #filters-panel.control-panel.style-panel.active
62
+ .control-panel-header
63
+ button.control-button onclick="toggleAllFilters()" Toggle All Filters
64
+ button.control-button onclick="toggleBasemap()" id="basemap-filters-btn" Show/Hide Basemap
65
+ .control-panel-content
66
+ #filter-buttons
67
+
68
+ #layers-panel.control-panel.style-panel
69
+ .control-panel-header
70
+ button.control-button onclick="toggleAllLayers()" Toggle All Layers
71
+ button.control-button onclick="toggleBasemap()" id="basemap-layers-btn" Show/Hide Basemap
72
+ .control-panel-content
73
+ #layer-buttons
74
+ button.control-section-toggle-external id="style-controls-toggle" onclick="toggleControlSection('style-controls')" title="Collapse Style Controls"
75
+ span.toggle-icon ◀
38
76
 
39
77
  #map-container
40
78
  #map.map-layer data-style-url="#{style_url}"
41
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
+
42
93
  #version-info.version-info
43
94
  a.version-info-version href="https://github.com/artyomb/maplibre-preview" target="_blank" v#{MapLibrePreview::VERSION}
44
95
 
@@ -77,11 +128,17 @@ ruby:
77
128
  javascript:
78
129
  const mapEl = document.getElementById('map');
79
130
  const style_url = mapEl?.dataset?.styleUrl || null;
80
- let showBasemapFilters = true, showBasemapLayers = true, currentMode = 'filters';
131
+ let showBasemap = true, currentMode = 'filters';
81
132
  let hoverMode = 'click';
82
133
  let layerStates = {};
83
134
  let map = null;
84
135
  let antialiasEnabled = localStorage.getItem('antialiasEnabled') !== 'false';
136
+ let mapCacheDisabled = localStorage.getItem('mapCacheDisabled') === 'true';
137
+ let basemapOpacity = Number.parseFloat(localStorage.getItem('basemapOpacity') || '0.8');
138
+ let terrainExaggeration = Number.parseFloat(localStorage.getItem('terrainExaggeration') || '1');
139
+ let collisionBoxesEnabled = localStorage.getItem('collisionBoxesEnabled') === 'true';
140
+ let overdrawInspectorEnabled = localStorage.getItem('overdrawInspectorEnabled') === 'true';
141
+ let tileFadeEnabled = localStorage.getItem('tileFadeEnabled') !== 'false';
85
142
  let styleLoaded = false, resourcesLoaded = 0, totalResources = 0;
86
143
  let tilesLoaded = 0, tilesTotal = 0;
87
144
  let currentStyle = null;
@@ -93,19 +150,295 @@ javascript:
93
150
  let currentProfile = null;
94
151
  let contourManager = null;
95
152
  let tileGridManager = null;
153
+ let originalStyle = null;
154
+ let styleParameterDefinitions = new Map();
155
+ let styleParameterValues = {};
156
+ let parameterizedUrlPrefixes = new Set();
96
157
 
97
158
  const toDomId = (prefix, id) => `${prefix}-${String(id).replace(/[^a-zA-Z0-9_-]/g, '_')}`;
159
+ const noCacheRequestOptions = () => mapCacheDisabled ? {cache: 'no-store'} : undefined;
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
+ };
98
424
 
99
425
  const getRealTerrainElevation = (lngLat) => {
100
426
  const elevation = map.queryTerrainElevation(lngLat);
101
427
  if (elevation == null) return null;
102
428
 
103
- const exaggeration = currentStyle?.terrain?.exaggeration || 1.0;
429
+ const exaggeration = terrainExaggeration || currentStyle?.terrain?.exaggeration || 1.0;
104
430
  return elevation / exaggeration;
105
431
  };
106
432
 
107
- const showLoading = () => document.getElementById('loading-indicator').style.display = 'block';
108
- 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
+ };
109
442
 
110
443
  const updateLoadingProgress = () => {
111
444
  let progress = 0;
@@ -136,7 +469,7 @@ javascript:
136
469
  const target = isStringArg ? null : (arg1?.currentTarget || arg1);
137
470
  currentMode = mode;
138
471
 
139
- document.querySelectorAll('.mode-button').forEach(btn => btn.classList.remove('active'));
472
+ document.querySelectorAll('#style-mode-switcher .mode-button').forEach(btn => btn.classList.remove('active'));
140
473
  (target || document.getElementById(`mode-${mode}`))?.classList.add('active');
141
474
 
142
475
  document.getElementById('filters-panel').classList.toggle('active', mode === 'filters');
@@ -147,6 +480,17 @@ javascript:
147
480
  updateBasemapButton();
148
481
  };
149
482
 
483
+ const switchSettingsMode = (arg1, arg2) => {
484
+ const isStringArg = typeof arg1 === 'string';
485
+ const mode = isStringArg ? arg1 : arg2;
486
+ const target = isStringArg ? null : (arg1?.currentTarget || arg1);
487
+ document.querySelectorAll('#settings-mode-switcher .mode-button').forEach(btn => btn.classList.remove('active'));
488
+ (target || document.getElementById(`settings-mode-${mode}`))?.classList.add('active');
489
+
490
+ document.querySelectorAll('.settings-panel').forEach(panel => panel.classList.remove('active'));
491
+ document.getElementById(`settings-${mode}-panel`)?.classList.add('active');
492
+ };
493
+
150
494
  const applyLayerMode = () => {
151
495
  Object.keys(layerStates).forEach(layerId => {
152
496
  if (map.getLayer(layerId)) {
@@ -156,8 +500,7 @@ javascript:
156
500
  };
157
501
 
158
502
  const applyBasemapVisibility = () => {
159
- const isVisible = currentMode === 'filters' ? showBasemapFilters : showBasemapLayers;
160
- map?.getLayer?.('preview-basemap-layer') && map.setLayoutProperty('preview-basemap-layer', 'visibility', isVisible ? 'visible' : 'none');
503
+ map?.getLayer?.('preview-basemap-layer') && map.setLayoutProperty('preview-basemap-layer', 'visibility', showBasemap ? 'visible' : 'none');
161
504
  };
162
505
 
163
506
  const checkStyleResources = (style) => {
@@ -196,7 +539,13 @@ javascript:
196
539
  zoom,
197
540
  attributionControl: false,
198
541
  validateStyle: false,
199
- antialias: antialiasEnabled
542
+ antialias: antialiasEnabled,
543
+ canvasContextAttributes: {antialias: antialiasEnabled},
544
+ fadeDuration: tileFadeEnabled ? 300 : 0,
545
+ transformRequest: (url) => ({
546
+ url: patchRequestUrl(url),
547
+ ...noCacheRequestOptions()
548
+ })
200
549
  });
201
550
  };
202
551
 
@@ -222,7 +571,7 @@ javascript:
222
571
  type: 'raster',
223
572
  source: 'preview-basemap',
224
573
  layout: {visibility: 'visible'},
225
- paint: {'raster-opacity': 0.8, 'raster-fade-duration': 300}
574
+ paint: {'raster-opacity': basemapOpacity, 'raster-fade-duration': tileFadeEnabled ? 300 : 0}
226
575
  });
227
576
 
228
577
  return modifiedStyle;
@@ -244,19 +593,21 @@ javascript:
244
593
  popup.setLngLat(e.lngLat).setHTML(tooltips.join('<br>')).addTo(map);
245
594
  };
246
595
 
247
- const initializeMap = () => {
596
+ const initializeMap = async () => {
248
597
  showLoading();
249
598
  const emptyStyle = {version: 8, sources: {}, layers: []};
250
599
 
251
- style_url
252
- ? fetch(style_url)
253
- .then(response => response.json())
254
- .then(originalStyle => createMapWithStyle(addBasemapToStyle(originalStyle)))
255
- .catch(error => {
256
- console.error('Style loading error:', error);
257
- createMapWithStyle(addBasemapToStyle(emptyStyle));
258
- })
259
- : 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)));
260
611
  };
261
612
 
262
613
  const createMapWithStyle = (style) => {
@@ -319,13 +670,7 @@ javascript:
319
670
 
320
671
  const onStyleReady = () => {
321
672
  const hasTerrain = currentStyle?.terrain;
322
- const projectionType = hasTerrain ? 'mercator' : 'globe';
323
-
324
- try {
325
- map.setProjection({type: projectionType});
326
- } catch (e) {
327
- console.warn('Projection setting failed:', e);
328
- }
673
+ applyDefaultProjection();
329
674
 
330
675
  try {
331
676
  map.addControl(new maplibregl.GlobeControl(), 'top-right');
@@ -336,14 +681,20 @@ javascript:
336
681
  if (currentStyle?.terrain) {
337
682
  const terrainSourceName = currentStyle.terrain.source;
338
683
  map.addControl(new maplibregl.TerrainControl({
339
- source: terrainSourceName
684
+ source: terrainSourceName,
685
+ exaggeration: terrainExaggeration
340
686
  }), 'top-right');
341
687
 
342
688
  initializeContourManager();
689
+ applyTerrainExaggeration();
343
690
  }
344
691
 
345
692
  initializeTileGridManager();
693
+ applyBasemapOpacity();
694
+ applyDebugRenderSettings();
695
+ applyTileFadeSetting();
346
696
  updateTerrainIndicator();
697
+ updateMapSettingsControls();
347
698
  };
348
699
 
349
700
  const setupMapInteractions = () => {
@@ -415,20 +766,20 @@ javascript:
415
766
 
416
767
  const setupPerformanceMonitoring = () => {
417
768
  [startPerformanceMonitoring, () => requestAnimationFrame(countFrame),
418
- updateHoverModeButton, updateBasemapButton, updateAntialiasButton].forEach(fn => fn());
769
+ updateHoverModeButton, updateBasemapButton, updateAntialiasButton, updateMapCacheButton,
770
+ updateMapSettingsControls].forEach(fn => fn());
419
771
  };
420
772
 
421
773
  const toggleBasemap = () => {
422
- currentMode === 'filters' ? showBasemapFilters = !showBasemapFilters : showBasemapLayers = !showBasemapLayers;
774
+ showBasemap = !showBasemap;
423
775
  applyBasemapVisibility();
424
776
  updateBasemapButton();
425
777
  };
426
778
 
427
779
  const updateBasemapButton = () => {
428
- const isVisible = currentMode === 'filters' ? showBasemapFilters : showBasemapLayers;
429
780
  document.querySelectorAll('button[onclick="toggleBasemap()"]').forEach(btn => {
430
- btn.textContent = isVisible ? 'Hide Basemap' : 'Show Basemap';
431
- btn.className = `control-button ${isVisible ? 'active' : 'inactive'}`;
781
+ btn.textContent = showBasemap ? 'Hide Basemap' : 'Show Basemap';
782
+ btn.className = `control-button ${showBasemap ? 'active' : 'inactive'}`;
432
783
  });
433
784
  };
434
785
 
@@ -436,6 +787,7 @@ javascript:
436
787
  const terrainRow = document.getElementById('terrain-row');
437
788
  const terrainElement = document.getElementById('terrain-status-value');
438
789
  const profileBtn = document.getElementById('profile-mode-btn');
790
+ const terrainSetting = document.getElementById('terrain-exaggeration-setting');
439
791
 
440
792
  if (!terrainRow || !terrainElement) return;
441
793
 
@@ -445,9 +797,11 @@ javascript:
445
797
  terrainElement.textContent = 'is detected';
446
798
  terrainElement.className = 'metric-value success';
447
799
  profileBtn && (profileBtn.style.display = 'block');
800
+ terrainSetting && (terrainSetting.style.display = 'flex');
448
801
  } else {
449
802
  terrainRow.style.display = 'none';
450
803
  profileBtn && (profileBtn.style.display = 'none');
804
+ terrainSetting && (terrainSetting.style.display = 'none');
451
805
  }
452
806
  };
453
807
 
@@ -594,6 +948,7 @@ javascript:
594
948
  [header, stats, chart].forEach(el => overlay.appendChild(el));
595
949
  document.getElementById('map-container').appendChild(overlay);
596
950
  setTimeout(() => drawSimpleProfileChart(profile), 10);
951
+ layoutBottomOverlays();
597
952
  };
598
953
 
599
954
  const drawSimpleProfileChart = (profile) => {
@@ -671,6 +1026,7 @@ javascript:
671
1026
 
672
1027
  const hideProfile = () => {
673
1028
  document.getElementById('profile-overlay')?.remove();
1029
+ layoutBottomOverlays();
674
1030
  map.getLayer('profile-line') && (map.removeLayer('profile-line'), map.removeSource('profile-line'));
675
1031
  map.getLayer('temporary-line') && (map.removeLayer('temporary-line'), map.removeSource('temporary-line'));
676
1032
  hideMapMarker();
@@ -704,7 +1060,7 @@ javascript:
704
1060
  });
705
1061
 
706
1062
  if (map.getLayer('preview-basemap-layer')) {
707
- showBasemapLayers = newState;
1063
+ showBasemap = newState;
708
1064
  }
709
1065
 
710
1066
  updateLayerButtons();
@@ -720,7 +1076,7 @@ javascript:
720
1076
  }
721
1077
 
722
1078
  if (layerId === 'preview-basemap-layer') {
723
- showBasemapLayers = layerStates[layerId];
1079
+ showBasemap = layerStates[layerId];
724
1080
  updateBasemapButton();
725
1081
  }
726
1082
 
@@ -871,19 +1227,247 @@ javascript:
871
1227
  btn.className = `control-button ${antialiasEnabled ? 'active' : 'inactive'}`;
872
1228
  };
873
1229
 
874
- let layerControlsCollapsed = false;
1230
+ const toggleMapCache = () => {
1231
+ mapCacheDisabled = !mapCacheDisabled;
1232
+ localStorage.setItem('mapCacheDisabled', mapCacheDisabled.toString());
1233
+ window.location.reload();
1234
+ };
1235
+
1236
+ const updateMapCacheButton = () => {
1237
+ const btn = document.getElementById('map-cache-btn');
1238
+ if (!btn) return;
1239
+
1240
+ btn.textContent = `Cache: ${mapCacheDisabled ? 'OFF' : 'ON'}`;
1241
+ btn.className = `control-button ${mapCacheDisabled ? 'cache-off' : 'active'}`;
1242
+ btn.title = mapCacheDisabled ? 'Browser cache is disabled for map requests' : 'Browser cache is enabled for map requests';
1243
+ };
1244
+
1245
+ const getDefaultProjectionType = () => {
1246
+ return currentStyle?.terrain ? 'mercator' : 'globe';
1247
+ };
1248
+
1249
+ const applyDefaultProjection = () => {
1250
+ if (!map) return;
1251
+
1252
+ try {
1253
+ map.setProjection({type: getDefaultProjectionType()});
1254
+ } catch (e) {
1255
+ console.warn('Projection setting failed:', e);
1256
+ }
1257
+ };
1258
+
1259
+ const setBasemapOpacity = (value) => {
1260
+ basemapOpacity = clamp(Number.parseFloat(value) / 100, 0, 1);
1261
+ localStorage.setItem('basemapOpacity', basemapOpacity.toString());
1262
+ applyBasemapOpacity();
1263
+ updateBasemapOpacityControl();
1264
+ };
1265
+
1266
+ const applyBasemapOpacity = () => {
1267
+ if (map?.getLayer?.('preview-basemap-layer')) {
1268
+ map.setPaintProperty('preview-basemap-layer', 'raster-opacity', basemapOpacity);
1269
+ }
1270
+ };
1271
+
1272
+ const updateBasemapOpacityControl = () => {
1273
+ const slider = document.getElementById('basemap-opacity-slider');
1274
+ const value = document.getElementById('basemap-opacity-value');
1275
+ const percent = Math.round(clamp(basemapOpacity, 0, 1) * 100);
1276
+ slider && (slider.value = percent);
1277
+ value && (value.textContent = `${percent}%`);
1278
+ };
1279
+
1280
+ const setTerrainExaggeration = (value) => {
1281
+ terrainExaggeration = clamp(Number.parseFloat(value), 0, 3);
1282
+ localStorage.setItem('terrainExaggeration', terrainExaggeration.toString());
1283
+ applyTerrainExaggeration();
1284
+ updateTerrainExaggerationControl();
1285
+ };
1286
+
1287
+ const applyTerrainExaggeration = () => {
1288
+ if (!map || !currentStyle?.terrain?.source) return;
1289
+
1290
+ try {
1291
+ map.setTerrain({
1292
+ source: currentStyle.terrain.source,
1293
+ exaggeration: terrainExaggeration
1294
+ });
1295
+ } catch (e) {
1296
+ console.warn('Terrain exaggeration setting failed:', e);
1297
+ }
1298
+ };
1299
+
1300
+ const updateTerrainExaggerationControl = () => {
1301
+ const slider = document.getElementById('terrain-exaggeration-slider');
1302
+ const value = document.getElementById('terrain-exaggeration-value');
1303
+ slider && (slider.value = terrainExaggeration);
1304
+ value && (value.textContent = `${terrainExaggeration.toFixed(1)}x`);
1305
+ };
1306
+
1307
+ const toggleCollisionBoxes = () => {
1308
+ collisionBoxesEnabled = !collisionBoxesEnabled;
1309
+ localStorage.setItem('collisionBoxesEnabled', collisionBoxesEnabled.toString());
1310
+ applyDebugRenderSettings();
1311
+ updateDebugRenderButtons();
1312
+ };
1313
+
1314
+ const toggleOverdrawInspector = () => {
1315
+ overdrawInspectorEnabled = !overdrawInspectorEnabled;
1316
+ localStorage.setItem('overdrawInspectorEnabled', overdrawInspectorEnabled.toString());
1317
+ applyDebugRenderSettings();
1318
+ updateDebugRenderButtons();
1319
+ };
1320
+
1321
+ const applyDebugRenderSettings = () => {
1322
+ if (!map) return;
1323
+ map.showCollisionBoxes = collisionBoxesEnabled;
1324
+ map.showOverdrawInspector = overdrawInspectorEnabled;
1325
+ };
1326
+
1327
+ const updateDebugRenderButtons = () => {
1328
+ const collisionBtn = document.getElementById('collision-boxes-btn');
1329
+ const overdrawBtn = document.getElementById('overdraw-inspector-btn');
1330
+ if (collisionBtn) {
1331
+ collisionBtn.textContent = `Collision Boxes: ${collisionBoxesEnabled ? 'ON' : 'OFF'}`;
1332
+ collisionBtn.className = `control-button ${collisionBoxesEnabled ? 'active' : 'inactive'}`;
1333
+ }
1334
+ if (overdrawBtn) {
1335
+ overdrawBtn.textContent = `Overdraw: ${overdrawInspectorEnabled ? 'ON' : 'OFF'}`;
1336
+ overdrawBtn.className = `control-button ${overdrawInspectorEnabled ? 'active' : 'inactive'}`;
1337
+ }
1338
+ };
1339
+
1340
+ const toggleTileFade = () => {
1341
+ tileFadeEnabled = !tileFadeEnabled;
1342
+ localStorage.setItem('tileFadeEnabled', tileFadeEnabled.toString());
1343
+ applyTileFadeSetting();
1344
+ updateTileFadeButton();
1345
+ };
1346
+
1347
+ const applyTileFadeSetting = () => {
1348
+ if (!map?.getStyle()?.layers) return;
1349
+ const fadeDuration = tileFadeEnabled ? 300 : 0;
1350
+
1351
+ map.getStyle().layers.forEach(layer => {
1352
+ if (layer.type === 'raster' && map.getLayer(layer.id)) {
1353
+ map.setPaintProperty(layer.id, 'raster-fade-duration', fadeDuration);
1354
+ }
1355
+ });
1356
+ };
1357
+
1358
+ const updateTileFadeButton = () => {
1359
+ const btn = document.getElementById('tile-fade-btn');
1360
+ if (!btn) return;
1361
+ btn.textContent = `Raster Fade: ${tileFadeEnabled ? 'ON' : 'OFF'}`;
1362
+ btn.className = `control-button ${tileFadeEnabled ? 'active' : 'inactive'}`;
1363
+ };
1364
+
1365
+ const updateMapSettingsControls = () => {
1366
+ updateBasemapOpacityControl();
1367
+ updateTerrainExaggerationControl();
1368
+ updateDebugRenderButtons();
1369
+ updateTileFadeButton();
1370
+ };
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
+
1454
+ const toggleControlSection = (sectionName) => {
1455
+ const wrapper = document.getElementById(`${sectionName}-wrapper`);
1456
+ const section = document.getElementById(`${sectionName}-section`);
1457
+ const externalToggle = document.getElementById(`${sectionName}-toggle`);
1458
+ const inlineToggle = document.getElementById(`${sectionName}-toggle-inline`);
1459
+ if (!wrapper || !section) return;
875
1460
 
876
- const toggleLayerControls = () => {
877
- const wrapper = document.querySelector('.layer-controls-wrapper');
878
- const controls = document.getElementById('layer-controls');
879
- if (!wrapper || !controls) return;
1461
+ const isCollapsed = section.classList.toggle('collapsed');
1462
+ wrapper.classList.toggle('collapsed', isCollapsed);
880
1463
 
881
- layerControlsCollapsed = !layerControlsCollapsed;
882
- controls.classList.toggle('collapsed', layerControlsCollapsed);
883
- wrapper.classList.toggle('collapsed', layerControlsCollapsed);
1464
+ const sectionTitle = section.querySelector('.control-section-title')?.textContent || 'section';
1465
+ externalToggle && (externalToggle.title = `Collapse ${sectionTitle}`);
1466
+ inlineToggle && (inlineToggle.title = `Expand ${sectionTitle}`);
884
1467
  };
885
1468
 
886
1469
  window.switchMode = switchMode;
1470
+ window.switchSettingsMode = switchSettingsMode;
887
1471
  window.toggleBasemap = toggleBasemap;
888
1472
  window.toggleHoverMode = toggleHoverMode;
889
1473
  window.toggleAllFilters = () => filters?.toggleAllFilters();
@@ -891,9 +1475,20 @@ javascript:
891
1475
  window.togglePerformancePanel = togglePerformancePanel;
892
1476
  window.toggleProfileMode = toggleProfileMode;
893
1477
  window.toggleAntialias = toggleAntialias;
894
- window.toggleLayerControls = toggleLayerControls;
1478
+ window.toggleMapCache = toggleMapCache;
1479
+ window.setBasemapOpacity = setBasemapOpacity;
1480
+ window.setTerrainExaggeration = setTerrainExaggeration;
1481
+ window.toggleCollisionBoxes = toggleCollisionBoxes;
1482
+ window.toggleOverdrawInspector = toggleOverdrawInspector;
1483
+ window.toggleTileFade = toggleTileFade;
1484
+ window.applyStyleParameters = applyStyleParameters;
1485
+ window.resetStyleParameters = resetStyleParameters;
1486
+ window.toggleStyleParametersPanel = toggleStyleParametersPanel;
1487
+ window.toggleControlSection = toggleControlSection;
1488
+ window.toggleLayerControls = () => toggleControlSection('style-controls');
895
1489
  window.hideProfile = hideProfile;
896
1490
  window.toggleTileGrid = toggleTileGrid;
897
1491
  window.tileGridManager = null;
1492
+ window.addEventListener('resize', layoutBottomOverlays);
898
1493
 
899
1494
  initializeMap();