maplibre-preview 1.4.3 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff0508a77186b4b42b987de36d9f307556f182cd184d62a206963d211719963f
4
- data.tar.gz: d547c84b4b1ca39ffe52b065a493dbec416d6aa5c5829c401bcafebd0dd9ad6f
3
+ metadata.gz: 17ba0e8d475ee1bb1a213790388e255df8eab1f4a3670382c0c77b3e2967f660
4
+ data.tar.gz: 4091e3a89317f63690f447167f68c5bdc887d442c33aad1dc418f7ed850a8abc
5
5
  SHA512:
6
- metadata.gz: 61b409020f21b43cf1b7cdb75ff6ea28980927d73cd60aa9e3acb7105eda17b9843c2789ef6eaa03437c8068b028441c01f78487b5e8df294fe0aecae478d4c1
7
- data.tar.gz: b066ef7e588b70d8ececa8eb3d90c66dae571f2ec15082dc2859b27ac3834d605b35f6e5722f4684fd0ba21ab21d45b21423de82794f3ae9d7f03a5459b849c4
6
+ metadata.gz: 43cb047740512a57492d2e8582e704f3b1a45c61bcc7add1fb8845eea75bf66e05e67e1d86f73003ca4a50aad59204857b6dd816789f68ddc0475cc99a0da4a6
7
+ data.tar.gz: 8188dbf8a672afd1f42ad70d51315b4741ce1006a2111f37b84ab71c8dfc8353ac34fb8920f72111ddc50ce7bdf038c90fccabcfe66a17d598fa2589bd84ba52
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.6.0] - 2026-05-13
4
+
5
+ ### Added
6
+ - **Basemap opacity control** - added a Map Settings slider for changing preview basemap transparency without changing the tested style
7
+ - **Terrain exaggeration control** - added a terrain-only slider that updates `map.setTerrain({ source, exaggeration })`
8
+ - **MapLibre debug controls** - added Collision Boxes, Overdraw, and Raster Fade toggles for label placement, dense style, and raster/DEM diagnostics
9
+
10
+ ### Changed
11
+ - **Tile Boundaries naming** - renamed the Tile Grid UI to Tile Boundaries to clarify that it uses MapLibre `showTileBoundaries`
12
+ - **Map Settings layout** - added compact setting rows and range styling for view-mode map controls
13
+
14
+ ## [1.5.1] - 2026-05-13
15
+
16
+ ### Changed
17
+ - **Map control layout** - split map settings and style controls into independent panels with separate collapse controls
18
+ - **Panel sizing** - adjusted map settings, filters, and layers panels to size from their visible content
19
+
20
+ ### Fixed
21
+ - **Filter and layer scrolling** - restored internal scrolling for large filter and layer lists after the panel layout split
22
+
23
+ ## [1.5.0] - 2026-05-13
24
+
25
+ ### Added
26
+ - **Map request cache toggle** - added a development mode that disables browser HTTP cache for style and MapLibre resource requests
27
+
28
+ ## [1.4.4] - 2026-05-13
29
+
30
+ ### Fixed
31
+ - **Filter labels** - normalize object-based localized labels to display `title`, `name`, or `label` instead of `[object Object]`
32
+
3
33
  ## [1.4.2] - 2026-04-23
4
34
 
5
35
  ### Changed
data/README.md CHANGED
@@ -125,6 +125,19 @@ The gem uses fixed configurations for optimal compatibility:
125
125
 
126
126
  **Style URL**: Pass via URL parameter `?style_url=https://example.com/style.json`
127
127
 
128
+ ## Map Settings
129
+
130
+ The preview UI keeps focused MapLibre controls that are useful for tile, style, and terrain services:
131
+
132
+ - **Basemap opacity**: changes the injected OpenStreetMap preview layer opacity while preserving the tested style.
133
+ - **Terrain exaggeration**: appears for styles with `terrain` and updates `map.setTerrain({ source, exaggeration })`.
134
+ - **Antialias**: reloads the WebGL context with antialiasing enabled or disabled.
135
+ - **Cache**: disables browser cache for style and map requests when comparing freshly generated tiles/styles.
136
+ - **Tile Boundaries**: toggles MapLibre tile boundary overlay, including tile coordinate/zoom diagnostics in pitched views, and visible tile count diagnostics.
137
+ - **Collision Boxes**: toggles MapLibre symbol collision boxes for label/icon debugging.
138
+ - **Overdraw**: toggles MapLibre overdraw inspection for dense mixed styles and expensive layers.
139
+ - **Raster Fade**: toggles raster tile fade duration for raster/DEM cache and reconstruction checks.
140
+
128
141
  ## API Reference
129
142
 
130
143
  ### Sinatra Extension
data/docs/README_RU.md CHANGED
@@ -125,6 +125,19 @@ Gem использует фиксированные настройки:
125
125
 
126
126
  **URL стиля**: Передается через параметр URL `?style_url=https://example.com/style.json`
127
127
 
128
+ ## Настройки карты
129
+
130
+ Интерфейс preview оставляет только те настройки MapLibre, которые полезны для сервисов тайлов, стилей и рельефа:
131
+
132
+ - **Basemap opacity**: меняет прозрачность добавленной preview-подложки OpenStreetMap, не меняя тестируемый стиль.
133
+ - **Terrain exaggeration**: появляется для стилей с `terrain` и обновляет `map.setTerrain({ source, exaggeration })`.
134
+ - **Antialias**: перезагружает WebGL context с включенным или выключенным antialiasing.
135
+ - **Cache**: отключает browser cache для style и map requests при проверке свежесгенерированных тайлов/стилей.
136
+ - **Tile Boundaries**: включает MapLibre overlay границ тайлов, включая диагностику координат/зумов тайлов при наклоне карты, и счетчик видимых тайлов.
137
+ - **Collision Boxes**: включает MapLibre symbol collision boxes для отладки подписей и иконок.
138
+ - **Overdraw**: включает MapLibre overdraw inspection для плотных mixed styles и дорогих слоев.
139
+ - **Raster Fade**: включает или выключает fade duration у raster tiles для проверки raster/DEM cache и reconstruction.
140
+
128
141
  ## Справочник API
129
142
 
130
143
  ### Расширение Sinatra
@@ -37,16 +37,28 @@ class Filters {
37
37
  const languagePriority = ['en-US', 'en', 'ru'];
38
38
 
39
39
  for (const lang of languagePriority) {
40
- if (locale[lang]?.[filterId]) return locale[lang][filterId];
40
+ const label = this.normalizeLocalizedFilterName(locale[lang]?.[filterId]);
41
+ if (label) return label;
41
42
  }
42
43
 
43
44
  for (const lang in locale) {
44
- if (locale[lang]?.[filterId]) return locale[lang][filterId];
45
+ const label = this.normalizeLocalizedFilterName(locale[lang]?.[filterId]);
46
+ if (label) return label;
45
47
  }
46
48
 
47
49
  return filterId;
48
50
  }
49
51
 
52
+ normalizeLocalizedFilterName(value) {
53
+ if (!value) return null;
54
+ if (typeof value === 'string') return value;
55
+ if (typeof value === 'object') {
56
+ return value.title || value.name || value.label || null;
57
+ }
58
+
59
+ return String(value);
60
+ }
61
+
50
62
  applyFilterMode() {
51
63
  if (this.isUpdating) return;
52
64
  this.isUpdating = true;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * TileGridManager - manages tile boundaries visualization and tile statistics
2
+ * TileGridManager - manages MapLibre tile boundary overlay and tile statistics
3
3
  */
4
4
  class TileGridManager {
5
5
  constructor(options) {
@@ -27,7 +27,7 @@ class TileGridManager {
27
27
 
28
28
  this.panelContainer.innerHTML = `
29
29
  <div class="tilegrid-header">
30
- <span class="tilegrid-title">Tile Grid</span>
30
+ <span class="tilegrid-title">Tile Boundaries</span>
31
31
  <button class="tilegrid-close" onclick="tileGridManager.toggle()">×</button>
32
32
  </div>
33
33
  <div class="tilegrid-stats">
@@ -36,7 +36,7 @@ class TileGridManager {
36
36
  </div>
37
37
  <div class="tilegrid-controls">
38
38
  <label class="tilegrid-checkbox-label">
39
- <span>Show tile borders</span>
39
+ <span>Show boundary overlay</span>
40
40
  <input type="checkbox" id="tilegrid-borders-checkbox" checked onchange="tileGridManager.toggleBorders(this.checked)">
41
41
  <span class="tilegrid-checkbox-custom"></span>
42
42
  </label>
@@ -118,7 +118,7 @@ class TileGridManager {
118
118
 
119
119
  const btn = document.getElementById('tilegrid-mode-btn');
120
120
  if (btn) {
121
- btn.textContent = this.isVisible ? 'Hide Tile Grid' : 'Tile Grid';
121
+ btn.textContent = this.isVisible ? 'Hide Tile Boundaries' : 'Tile Boundaries';
122
122
  btn.className = `control-button ${this.isVisible ? 'active' : ''}`;
123
123
  }
124
124
 
@@ -171,4 +171,3 @@ class TileGridManager {
171
171
  }
172
172
  }
173
173
  }
174
-
@@ -1,3 +1,3 @@
1
1
  module MapLibrePreview
2
- VERSION = '1.4.3'
2
+ VERSION = '1.6.0'
3
3
  end
@@ -175,37 +175,80 @@ html
175
175
  | .controls a:hover { text-decoration: underline; }
176
176
  | .layer-controls-wrapper {
177
177
  | position: absolute; top: 50%; left: 10px; transform: translateY(-50%); z-index: 1000;
178
- | display: flex; align-items: center; gap: 0;
178
+ | display: flex; align-items: flex-start; gap: 0;
179
179
  | }
180
180
  | .layer-controls {
181
- | background: rgba(60, 63, 65, 0.95); border: 1px solid #555555; border-right: none; padding: 15px;
182
- | border-radius: 4px; color: #a9b7c6; display: flex; flex-direction: column; gap: 6px;
183
- | max-width: 300px; max-height: 80vh; transition: max-width 0.2s;
184
- | overflow: hidden; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); order: 1;
181
+ | background: transparent; border: none; padding: 0;
182
+ | border-radius: 4px; color: #a9b7c6; display: flex; flex-direction: column; gap: 12px;
183
+ | width: fit-content; max-width: min(90vw, 420px); max-height: 80vh; transition: max-width 0.2s;
184
+ | overflow: hidden; order: 1;
185
185
  | }
186
- | .layer-controls.collapsed {
187
- | max-width: 0; padding: 15px 0; border: none; opacity: 0; pointer-events: none;
186
+ | .control-section-wrapper {
187
+ | display: flex; align-items: center; width: max-content; max-width: min(90vw, 420px);
188
188
  | }
189
- | .layer-controls-toggle {
189
+ | .control-section-wrapper + .control-section-wrapper {
190
+ | margin-top: 4px;
191
+ | }
192
+ | .control-section {
193
+ | display: flex; flex-direction: column; gap: 8px; min-height: 0; width: max-content;
194
+ | max-width: min(90vw, 420px);
195
+ | background: rgba(60, 63, 65, 0.95); border: 1px solid #555555; padding: 12px;
196
+ | border-radius: 4px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
197
+ | }
198
+ | .control-section-wrapper:not(.collapsed) .control-section {
199
+ | border-right: none; border-radius: 4px 0 0 4px;
200
+ | }
201
+ | .control-section.collapsed {
202
+ | gap: 0;
203
+ | }
204
+ | .control-section-header {
205
+ | display: flex; align-items: center; justify-content: space-between; gap: 8px;
206
+ | }
207
+ | .control-section-title {
208
+ | color: #ffc66d; font-size: 11px; font-weight: bold; line-height: 1.2;
209
+ | text-transform: uppercase;
210
+ | }
211
+ | .control-section-toggle-external {
190
212
  | background: rgba(60, 63, 65, 0.95); border: 1px solid #555555;
191
213
  | border-left: none; border-radius: 0 4px 4px 0; padding: 8px 4px;
192
214
  | color: #a9b7c6; cursor: pointer; display: flex; align-items: center; justify-content: center;
193
- | width: 18px; height: auto; min-height: 60px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); order: 2;
215
+ | width: 18px; height: auto; min-height: 60px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); flex-shrink: 0;
194
216
  | }
195
- | .layer-controls-toggle:hover {
217
+ | .control-section-toggle-external:hover {
196
218
  | background: rgba(75, 77, 79, 0.95); color: #ffc66d; border-color: #666666;
197
219
  | }
198
- | .layer-controls-wrapper.collapsed .layer-controls-toggle {
199
- | order: 0; border-left: 1px solid #555555; border-right: 1px solid #555555; border-radius: 4px;
220
+ | .control-section-wrapper.collapsed .control-section-toggle-external {
221
+ | display: none;
200
222
  | }
201
- | .layer-controls-wrapper.collapsed .toggle-icon {
202
- | transform: rotate(180deg);
223
+ | .control-section-toggle-inline {
224
+ | background: #4b4d4f; color: #a9b7c6; border: 1px solid #555555;
225
+ | border-radius: 3px; cursor: pointer; font-size: 10px; line-height: 1;
226
+ | padding: 0; width: 22px; height: 22px; transition: all 0.2s ease; flex-shrink: 0;
227
+ | display: none; align-items: center; justify-content: center;
228
+ | }
229
+ | .control-section-toggle-inline:hover {
230
+ | background: #5a5d5f; border-color: #666666;
231
+ | }
232
+ | .control-section.collapsed .control-section-toggle-inline {
233
+ | display: flex;
203
234
  | }
204
235
  | .toggle-icon {
205
236
  | font-size: 12px; font-weight: bold; transition: transform 0.2s;
206
237
  | }
238
+ | .control-section-body {
239
+ | display: flex; flex-direction: column; gap: 8px; min-height: 0;
240
+ | }
241
+ | .control-section.collapsed .control-section-body {
242
+ | display: none;
243
+ | }
244
+ | .map-settings-section {
245
+ | flex-shrink: 0;
246
+ | }
247
+ | .style-controls-section {
248
+ | overflow: hidden; min-width: 160px; max-height: calc(80vh - 120px);
249
+ | }
207
250
  | .mode-switcher {
208
- | display: flex; gap: 2px; margin-bottom: 10px; background: #3c3f41;
251
+ | display: flex; gap: 2px; margin-bottom: 10px; background: #3c3f41; width: 100%;
209
252
  | border-radius: 3px; padding: 2px; flex-shrink: 0;
210
253
  | }
211
254
  | .mode-button {
@@ -218,6 +261,27 @@ html
218
261
  | display: none; flex-direction: column; gap: 6px;
219
262
  | max-height: calc(80vh - 200px); overflow-y: auto;
220
263
  | }
264
+ | .settings-panel {
265
+ | max-height: none; overflow: visible; width: 220px;
266
+ | }
267
+ | .setting-group {
268
+ | display: flex; flex-direction: column; gap: 6px; padding-bottom: 8px;
269
+ | border-bottom: 1px solid #464749;
270
+ | }
271
+ | .setting-label {
272
+ | display: flex; align-items: center; justify-content: space-between; gap: 8px;
273
+ | color: #a9b7c6; font-size: 11px; font-weight: bold;
274
+ | }
275
+ | .setting-value {
276
+ | color: #6897bb; font-family: 'Courier New', monospace; font-weight: bold;
277
+ | }
278
+ | .setting-range {
279
+ | width: 100%; accent-color: #6897bb; cursor: pointer;
280
+ | }
281
+ | .style-panel {
282
+ | flex: 1; min-height: 0; max-height: calc(80vh - 245px); width: max-content; max-width: 100%;
283
+ | overflow: hidden;
284
+ | }
221
285
  | .control-panel::-webkit-scrollbar { width: 8px; }
222
286
  | .control-panel::-webkit-scrollbar-track { background: #3c3f41; border-radius: 4px; }
223
287
  | .control-panel::-webkit-scrollbar-thumb {
@@ -231,16 +295,26 @@ html
231
295
  | display: flex; flex-direction: column; gap: 6px;
232
296
  | }
233
297
  | .control-panel-content {
234
- | flex: 1; overflow-y: auto;
298
+ | flex: 1; min-height: 0; max-height: calc(80vh - 325px); overflow-y: auto;
299
+ | width: max-content; max-width: 100%;
300
+ | }
301
+ | #filter-buttons, #layer-buttons {
302
+ | display: inline-grid; grid-template-columns: max-content; gap: 2px; width: max-content;
303
+ | justify-items: stretch;
235
304
  | }
236
305
  | .control-button {
237
306
  | background: #4b4d4f; color: #a9b7c6; border: 1px solid #555555;
238
307
  | padding: 6px 12px; border-radius: 3px; cursor: pointer; font-size: 11px;
239
- | transition: all 0.2s ease; width: 100%; box-sizing: border-box;
308
+ | transition: all 0.2s ease; width: 100%; box-sizing: border-box; white-space: nowrap;
309
+ | }
310
+ | #filter-buttons .control-button, #layer-buttons .control-button {
311
+ | width: 100%; text-align: center;
240
312
  | }
241
313
  | .control-button:hover { background: #5a5d5f; border-color: #666666; }
242
314
  | .control-button.active { background: #6a9955; color: #ffffff; border-color: #7bb366; }
243
315
  | .control-button.inactive { background: #3c3f41; color: #808080; border-color: #464749; }
316
+ | .control-button.cache-off { background: #7a2f35; color: #ffffff; border-color: #9b3f47; font-weight: bold; }
317
+ | .control-button.cache-off:hover { background: #8f3841; border-color: #b64b55; }
244
318
  | .filter-group {
245
319
  | margin-bottom: 8px;
246
320
  | border: 1px solid #555555;
@@ -12,29 +12,67 @@ 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}"
@@ -77,11 +115,17 @@ ruby:
77
115
  javascript:
78
116
  const mapEl = document.getElementById('map');
79
117
  const style_url = mapEl?.dataset?.styleUrl || null;
80
- let showBasemapFilters = true, showBasemapLayers = true, currentMode = 'filters';
118
+ let showBasemap = true, currentMode = 'filters';
81
119
  let hoverMode = 'click';
82
120
  let layerStates = {};
83
121
  let map = null;
84
122
  let antialiasEnabled = localStorage.getItem('antialiasEnabled') !== 'false';
123
+ let mapCacheDisabled = localStorage.getItem('mapCacheDisabled') === 'true';
124
+ let basemapOpacity = Number.parseFloat(localStorage.getItem('basemapOpacity') || '0.8');
125
+ let terrainExaggeration = Number.parseFloat(localStorage.getItem('terrainExaggeration') || '1');
126
+ let collisionBoxesEnabled = localStorage.getItem('collisionBoxesEnabled') === 'true';
127
+ let overdrawInspectorEnabled = localStorage.getItem('overdrawInspectorEnabled') === 'true';
128
+ let tileFadeEnabled = localStorage.getItem('tileFadeEnabled') !== 'false';
85
129
  let styleLoaded = false, resourcesLoaded = 0, totalResources = 0;
86
130
  let tilesLoaded = 0, tilesTotal = 0;
87
131
  let currentStyle = null;
@@ -95,12 +139,14 @@ javascript:
95
139
  let tileGridManager = null;
96
140
 
97
141
  const toDomId = (prefix, id) => `${prefix}-${String(id).replace(/[^a-zA-Z0-9_-]/g, '_')}`;
142
+ const noCacheRequestOptions = () => mapCacheDisabled ? {cache: 'no-store'} : undefined;
143
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
98
144
 
99
145
  const getRealTerrainElevation = (lngLat) => {
100
146
  const elevation = map.queryTerrainElevation(lngLat);
101
147
  if (elevation == null) return null;
102
148
 
103
- const exaggeration = currentStyle?.terrain?.exaggeration || 1.0;
149
+ const exaggeration = terrainExaggeration || currentStyle?.terrain?.exaggeration || 1.0;
104
150
  return elevation / exaggeration;
105
151
  };
106
152
 
@@ -136,7 +182,7 @@ javascript:
136
182
  const target = isStringArg ? null : (arg1?.currentTarget || arg1);
137
183
  currentMode = mode;
138
184
 
139
- document.querySelectorAll('.mode-button').forEach(btn => btn.classList.remove('active'));
185
+ document.querySelectorAll('#style-mode-switcher .mode-button').forEach(btn => btn.classList.remove('active'));
140
186
  (target || document.getElementById(`mode-${mode}`))?.classList.add('active');
141
187
 
142
188
  document.getElementById('filters-panel').classList.toggle('active', mode === 'filters');
@@ -147,6 +193,17 @@ javascript:
147
193
  updateBasemapButton();
148
194
  };
149
195
 
196
+ const switchSettingsMode = (arg1, arg2) => {
197
+ const isStringArg = typeof arg1 === 'string';
198
+ const mode = isStringArg ? arg1 : arg2;
199
+ const target = isStringArg ? null : (arg1?.currentTarget || arg1);
200
+ document.querySelectorAll('#settings-mode-switcher .mode-button').forEach(btn => btn.classList.remove('active'));
201
+ (target || document.getElementById(`settings-mode-${mode}`))?.classList.add('active');
202
+
203
+ document.querySelectorAll('.settings-panel').forEach(panel => panel.classList.remove('active'));
204
+ document.getElementById(`settings-${mode}-panel`)?.classList.add('active');
205
+ };
206
+
150
207
  const applyLayerMode = () => {
151
208
  Object.keys(layerStates).forEach(layerId => {
152
209
  if (map.getLayer(layerId)) {
@@ -156,8 +213,7 @@ javascript:
156
213
  };
157
214
 
158
215
  const applyBasemapVisibility = () => {
159
- const isVisible = currentMode === 'filters' ? showBasemapFilters : showBasemapLayers;
160
- map?.getLayer?.('preview-basemap-layer') && map.setLayoutProperty('preview-basemap-layer', 'visibility', isVisible ? 'visible' : 'none');
216
+ map?.getLayer?.('preview-basemap-layer') && map.setLayoutProperty('preview-basemap-layer', 'visibility', showBasemap ? 'visible' : 'none');
161
217
  };
162
218
 
163
219
  const checkStyleResources = (style) => {
@@ -196,7 +252,13 @@ javascript:
196
252
  zoom,
197
253
  attributionControl: false,
198
254
  validateStyle: false,
199
- antialias: antialiasEnabled
255
+ antialias: antialiasEnabled,
256
+ canvasContextAttributes: {antialias: antialiasEnabled},
257
+ fadeDuration: tileFadeEnabled ? 300 : 0,
258
+ transformRequest: (url) => ({
259
+ url,
260
+ ...noCacheRequestOptions()
261
+ })
200
262
  });
201
263
  };
202
264
 
@@ -222,7 +284,7 @@ javascript:
222
284
  type: 'raster',
223
285
  source: 'preview-basemap',
224
286
  layout: {visibility: 'visible'},
225
- paint: {'raster-opacity': 0.8, 'raster-fade-duration': 300}
287
+ paint: {'raster-opacity': basemapOpacity, 'raster-fade-duration': tileFadeEnabled ? 300 : 0}
226
288
  });
227
289
 
228
290
  return modifiedStyle;
@@ -249,7 +311,7 @@ javascript:
249
311
  const emptyStyle = {version: 8, sources: {}, layers: []};
250
312
 
251
313
  style_url
252
- ? fetch(style_url)
314
+ ? fetch(style_url, noCacheRequestOptions())
253
315
  .then(response => response.json())
254
316
  .then(originalStyle => createMapWithStyle(addBasemapToStyle(originalStyle)))
255
317
  .catch(error => {
@@ -319,13 +381,7 @@ javascript:
319
381
 
320
382
  const onStyleReady = () => {
321
383
  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
- }
384
+ applyDefaultProjection();
329
385
 
330
386
  try {
331
387
  map.addControl(new maplibregl.GlobeControl(), 'top-right');
@@ -336,14 +392,20 @@ javascript:
336
392
  if (currentStyle?.terrain) {
337
393
  const terrainSourceName = currentStyle.terrain.source;
338
394
  map.addControl(new maplibregl.TerrainControl({
339
- source: terrainSourceName
395
+ source: terrainSourceName,
396
+ exaggeration: terrainExaggeration
340
397
  }), 'top-right');
341
398
 
342
399
  initializeContourManager();
400
+ applyTerrainExaggeration();
343
401
  }
344
402
 
345
403
  initializeTileGridManager();
404
+ applyBasemapOpacity();
405
+ applyDebugRenderSettings();
406
+ applyTileFadeSetting();
346
407
  updateTerrainIndicator();
408
+ updateMapSettingsControls();
347
409
  };
348
410
 
349
411
  const setupMapInteractions = () => {
@@ -415,20 +477,20 @@ javascript:
415
477
 
416
478
  const setupPerformanceMonitoring = () => {
417
479
  [startPerformanceMonitoring, () => requestAnimationFrame(countFrame),
418
- updateHoverModeButton, updateBasemapButton, updateAntialiasButton].forEach(fn => fn());
480
+ updateHoverModeButton, updateBasemapButton, updateAntialiasButton, updateMapCacheButton,
481
+ updateMapSettingsControls].forEach(fn => fn());
419
482
  };
420
483
 
421
484
  const toggleBasemap = () => {
422
- currentMode === 'filters' ? showBasemapFilters = !showBasemapFilters : showBasemapLayers = !showBasemapLayers;
485
+ showBasemap = !showBasemap;
423
486
  applyBasemapVisibility();
424
487
  updateBasemapButton();
425
488
  };
426
489
 
427
490
  const updateBasemapButton = () => {
428
- const isVisible = currentMode === 'filters' ? showBasemapFilters : showBasemapLayers;
429
491
  document.querySelectorAll('button[onclick="toggleBasemap()"]').forEach(btn => {
430
- btn.textContent = isVisible ? 'Hide Basemap' : 'Show Basemap';
431
- btn.className = `control-button ${isVisible ? 'active' : 'inactive'}`;
492
+ btn.textContent = showBasemap ? 'Hide Basemap' : 'Show Basemap';
493
+ btn.className = `control-button ${showBasemap ? 'active' : 'inactive'}`;
432
494
  });
433
495
  };
434
496
 
@@ -436,6 +498,7 @@ javascript:
436
498
  const terrainRow = document.getElementById('terrain-row');
437
499
  const terrainElement = document.getElementById('terrain-status-value');
438
500
  const profileBtn = document.getElementById('profile-mode-btn');
501
+ const terrainSetting = document.getElementById('terrain-exaggeration-setting');
439
502
 
440
503
  if (!terrainRow || !terrainElement) return;
441
504
 
@@ -445,9 +508,11 @@ javascript:
445
508
  terrainElement.textContent = 'is detected';
446
509
  terrainElement.className = 'metric-value success';
447
510
  profileBtn && (profileBtn.style.display = 'block');
511
+ terrainSetting && (terrainSetting.style.display = 'flex');
448
512
  } else {
449
513
  terrainRow.style.display = 'none';
450
514
  profileBtn && (profileBtn.style.display = 'none');
515
+ terrainSetting && (terrainSetting.style.display = 'none');
451
516
  }
452
517
  };
453
518
 
@@ -704,7 +769,7 @@ javascript:
704
769
  });
705
770
 
706
771
  if (map.getLayer('preview-basemap-layer')) {
707
- showBasemapLayers = newState;
772
+ showBasemap = newState;
708
773
  }
709
774
 
710
775
  updateLayerButtons();
@@ -720,7 +785,7 @@ javascript:
720
785
  }
721
786
 
722
787
  if (layerId === 'preview-basemap-layer') {
723
- showBasemapLayers = layerStates[layerId];
788
+ showBasemap = layerStates[layerId];
724
789
  updateBasemapButton();
725
790
  }
726
791
 
@@ -871,19 +936,165 @@ javascript:
871
936
  btn.className = `control-button ${antialiasEnabled ? 'active' : 'inactive'}`;
872
937
  };
873
938
 
874
- let layerControlsCollapsed = false;
939
+ const toggleMapCache = () => {
940
+ mapCacheDisabled = !mapCacheDisabled;
941
+ localStorage.setItem('mapCacheDisabled', mapCacheDisabled.toString());
942
+ window.location.reload();
943
+ };
944
+
945
+ const updateMapCacheButton = () => {
946
+ const btn = document.getElementById('map-cache-btn');
947
+ if (!btn) return;
948
+
949
+ btn.textContent = `Cache: ${mapCacheDisabled ? 'OFF' : 'ON'}`;
950
+ btn.className = `control-button ${mapCacheDisabled ? 'cache-off' : 'active'}`;
951
+ btn.title = mapCacheDisabled ? 'Browser cache is disabled for map requests' : 'Browser cache is enabled for map requests';
952
+ };
953
+
954
+ const getDefaultProjectionType = () => {
955
+ return currentStyle?.terrain ? 'mercator' : 'globe';
956
+ };
957
+
958
+ const applyDefaultProjection = () => {
959
+ if (!map) return;
960
+
961
+ try {
962
+ map.setProjection({type: getDefaultProjectionType()});
963
+ } catch (e) {
964
+ console.warn('Projection setting failed:', e);
965
+ }
966
+ };
967
+
968
+ const setBasemapOpacity = (value) => {
969
+ basemapOpacity = clamp(Number.parseFloat(value) / 100, 0, 1);
970
+ localStorage.setItem('basemapOpacity', basemapOpacity.toString());
971
+ applyBasemapOpacity();
972
+ updateBasemapOpacityControl();
973
+ };
974
+
975
+ const applyBasemapOpacity = () => {
976
+ if (map?.getLayer?.('preview-basemap-layer')) {
977
+ map.setPaintProperty('preview-basemap-layer', 'raster-opacity', basemapOpacity);
978
+ }
979
+ };
980
+
981
+ const updateBasemapOpacityControl = () => {
982
+ const slider = document.getElementById('basemap-opacity-slider');
983
+ const value = document.getElementById('basemap-opacity-value');
984
+ const percent = Math.round(clamp(basemapOpacity, 0, 1) * 100);
985
+ slider && (slider.value = percent);
986
+ value && (value.textContent = `${percent}%`);
987
+ };
988
+
989
+ const setTerrainExaggeration = (value) => {
990
+ terrainExaggeration = clamp(Number.parseFloat(value), 0, 3);
991
+ localStorage.setItem('terrainExaggeration', terrainExaggeration.toString());
992
+ applyTerrainExaggeration();
993
+ updateTerrainExaggerationControl();
994
+ };
995
+
996
+ const applyTerrainExaggeration = () => {
997
+ if (!map || !currentStyle?.terrain?.source) return;
998
+
999
+ try {
1000
+ map.setTerrain({
1001
+ source: currentStyle.terrain.source,
1002
+ exaggeration: terrainExaggeration
1003
+ });
1004
+ } catch (e) {
1005
+ console.warn('Terrain exaggeration setting failed:', e);
1006
+ }
1007
+ };
1008
+
1009
+ const updateTerrainExaggerationControl = () => {
1010
+ const slider = document.getElementById('terrain-exaggeration-slider');
1011
+ const value = document.getElementById('terrain-exaggeration-value');
1012
+ slider && (slider.value = terrainExaggeration);
1013
+ value && (value.textContent = `${terrainExaggeration.toFixed(1)}x`);
1014
+ };
1015
+
1016
+ const toggleCollisionBoxes = () => {
1017
+ collisionBoxesEnabled = !collisionBoxesEnabled;
1018
+ localStorage.setItem('collisionBoxesEnabled', collisionBoxesEnabled.toString());
1019
+ applyDebugRenderSettings();
1020
+ updateDebugRenderButtons();
1021
+ };
1022
+
1023
+ const toggleOverdrawInspector = () => {
1024
+ overdrawInspectorEnabled = !overdrawInspectorEnabled;
1025
+ localStorage.setItem('overdrawInspectorEnabled', overdrawInspectorEnabled.toString());
1026
+ applyDebugRenderSettings();
1027
+ updateDebugRenderButtons();
1028
+ };
1029
+
1030
+ const applyDebugRenderSettings = () => {
1031
+ if (!map) return;
1032
+ map.showCollisionBoxes = collisionBoxesEnabled;
1033
+ map.showOverdrawInspector = overdrawInspectorEnabled;
1034
+ };
1035
+
1036
+ const updateDebugRenderButtons = () => {
1037
+ const collisionBtn = document.getElementById('collision-boxes-btn');
1038
+ const overdrawBtn = document.getElementById('overdraw-inspector-btn');
1039
+ if (collisionBtn) {
1040
+ collisionBtn.textContent = `Collision Boxes: ${collisionBoxesEnabled ? 'ON' : 'OFF'}`;
1041
+ collisionBtn.className = `control-button ${collisionBoxesEnabled ? 'active' : 'inactive'}`;
1042
+ }
1043
+ if (overdrawBtn) {
1044
+ overdrawBtn.textContent = `Overdraw: ${overdrawInspectorEnabled ? 'ON' : 'OFF'}`;
1045
+ overdrawBtn.className = `control-button ${overdrawInspectorEnabled ? 'active' : 'inactive'}`;
1046
+ }
1047
+ };
1048
+
1049
+ const toggleTileFade = () => {
1050
+ tileFadeEnabled = !tileFadeEnabled;
1051
+ localStorage.setItem('tileFadeEnabled', tileFadeEnabled.toString());
1052
+ applyTileFadeSetting();
1053
+ updateTileFadeButton();
1054
+ };
1055
+
1056
+ const applyTileFadeSetting = () => {
1057
+ if (!map?.getStyle()?.layers) return;
1058
+ const fadeDuration = tileFadeEnabled ? 300 : 0;
1059
+
1060
+ map.getStyle().layers.forEach(layer => {
1061
+ if (layer.type === 'raster' && map.getLayer(layer.id)) {
1062
+ map.setPaintProperty(layer.id, 'raster-fade-duration', fadeDuration);
1063
+ }
1064
+ });
1065
+ };
1066
+
1067
+ const updateTileFadeButton = () => {
1068
+ const btn = document.getElementById('tile-fade-btn');
1069
+ if (!btn) return;
1070
+ btn.textContent = `Raster Fade: ${tileFadeEnabled ? 'ON' : 'OFF'}`;
1071
+ btn.className = `control-button ${tileFadeEnabled ? 'active' : 'inactive'}`;
1072
+ };
1073
+
1074
+ const updateMapSettingsControls = () => {
1075
+ updateBasemapOpacityControl();
1076
+ updateTerrainExaggerationControl();
1077
+ updateDebugRenderButtons();
1078
+ updateTileFadeButton();
1079
+ };
1080
+
1081
+ const toggleControlSection = (sectionName) => {
1082
+ const wrapper = document.getElementById(`${sectionName}-wrapper`);
1083
+ const section = document.getElementById(`${sectionName}-section`);
1084
+ const externalToggle = document.getElementById(`${sectionName}-toggle`);
1085
+ const inlineToggle = document.getElementById(`${sectionName}-toggle-inline`);
1086
+ if (!wrapper || !section) return;
875
1087
 
876
- const toggleLayerControls = () => {
877
- const wrapper = document.querySelector('.layer-controls-wrapper');
878
- const controls = document.getElementById('layer-controls');
879
- if (!wrapper || !controls) return;
1088
+ const isCollapsed = section.classList.toggle('collapsed');
1089
+ wrapper.classList.toggle('collapsed', isCollapsed);
880
1090
 
881
- layerControlsCollapsed = !layerControlsCollapsed;
882
- controls.classList.toggle('collapsed', layerControlsCollapsed);
883
- wrapper.classList.toggle('collapsed', layerControlsCollapsed);
1091
+ const sectionTitle = section.querySelector('.control-section-title')?.textContent || 'section';
1092
+ externalToggle && (externalToggle.title = `Collapse ${sectionTitle}`);
1093
+ inlineToggle && (inlineToggle.title = `Expand ${sectionTitle}`);
884
1094
  };
885
1095
 
886
1096
  window.switchMode = switchMode;
1097
+ window.switchSettingsMode = switchSettingsMode;
887
1098
  window.toggleBasemap = toggleBasemap;
888
1099
  window.toggleHoverMode = toggleHoverMode;
889
1100
  window.toggleAllFilters = () => filters?.toggleAllFilters();
@@ -891,7 +1102,14 @@ javascript:
891
1102
  window.togglePerformancePanel = togglePerformancePanel;
892
1103
  window.toggleProfileMode = toggleProfileMode;
893
1104
  window.toggleAntialias = toggleAntialias;
894
- window.toggleLayerControls = toggleLayerControls;
1105
+ window.toggleMapCache = toggleMapCache;
1106
+ window.setBasemapOpacity = setBasemapOpacity;
1107
+ window.setTerrainExaggeration = setTerrainExaggeration;
1108
+ window.toggleCollisionBoxes = toggleCollisionBoxes;
1109
+ window.toggleOverdrawInspector = toggleOverdrawInspector;
1110
+ window.toggleTileFade = toggleTileFade;
1111
+ window.toggleControlSection = toggleControlSection;
1112
+ window.toggleLayerControls = () => toggleControlSection('style-controls');
895
1113
  window.hideProfile = hideProfile;
896
1114
  window.toggleTileGrid = toggleTileGrid;
897
1115
  window.tileGridManager = null;
@@ -16,6 +16,37 @@ RSpec.describe MapLibrePreview do
16
16
  expect(last_response.body).to include('d3')
17
17
  end
18
18
 
19
+ it 'renders map cache toggle wiring' do
20
+ get '/?style_url=https://example.com/style.json'
21
+ expect(last_response).to be_ok
22
+
23
+ expect(last_response.body).to include('Map Settings')
24
+ expect(last_response.body).to include('Style Controls')
25
+ expect(last_response.body).to include('id="settings-mode-switcher"')
26
+ expect(last_response.body).to include('id="style-mode-switcher"')
27
+ expect(last_response.body).to include('id="map-settings-toggle"')
28
+ expect(last_response.body).to include('id="style-controls-toggle"')
29
+ expect(last_response.body).to include('toggleControlSection')
30
+ expect(last_response.body).to include('id="map-cache-btn"')
31
+ expect(last_response.body).to include('id="basemap-opacity-slider"')
32
+ expect(last_response.body).to include('id="terrain-exaggeration-slider"')
33
+ expect(last_response.body).to include('id="collision-boxes-btn"')
34
+ expect(last_response.body).to include('id="overdraw-inspector-btn"')
35
+ expect(last_response.body).to include('id="tile-fade-btn"')
36
+ expect(last_response.body).to include('mapCacheDisabled')
37
+ expect(last_response.body).to include('basemapOpacity')
38
+ expect(last_response.body).to include('terrainExaggeration')
39
+ expect(last_response.body).to include('showCollisionBoxes')
40
+ expect(last_response.body).to include('showOverdrawInspector')
41
+ expect(last_response.body).to include("cache: 'no-store'")
42
+ expect(last_response.body).to include('cache-off')
43
+ expect(last_response.body).to include('transformRequest')
44
+ expect(last_response.body).to include('canvasContextAttributes')
45
+ expect(last_response.body).to include('raster-fade-duration')
46
+ expect(last_response.body).to include('window.toggleMapCache')
47
+ expect(last_response.body).to include('window.switchSettingsMode')
48
+ end
49
+
19
50
  it 'serves all required JavaScript modules' do
20
51
  %w[/js/filters.js /js/contour.js /js/tilegrid.js /vendor/maplibre-gl/maplibre-gl.js /vendor/maplibre-contour/index.min.js /vendor/d3/d3.v7.min.js].each do |js_file|
21
52
  get js_file
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.3
4
+ version: 1.6.0
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-04-23 00:00:00.000000000 Z
11
+ date: 2026-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack