maplibre-preview 0.0.2 → 1.1.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.
@@ -0,0 +1,323 @@
1
+ class Filters {
2
+ constructor(options) {
3
+ this.map = options.map;
4
+ this.container = options.container;
5
+ this.element_template = options.element_template || (title => `<div class="element">${title}</div>`);
6
+ this.group_template = options.group_template || (title => `<div class="group">${title}</div>`);
7
+
8
+ this.filterStates = {};
9
+ this.subFilterStatesBeforeGroupToggle = {};
10
+ this.currentStyle = null;
11
+ this.currentMode = 'filters';
12
+ this.isUpdating = false;
13
+ }
14
+
15
+ init() {
16
+ if (!this.map) return;
17
+ try {
18
+ this.currentStyle = this.map.getStyle();
19
+ if (this.currentStyle) {
20
+ this.createFilterButtons();
21
+ this.applyFilterMode();
22
+ }
23
+ } catch (e) {
24
+ console.warn('Could not get style for filters:', e);
25
+ }
26
+ }
27
+
28
+ setMode(mode) {
29
+ this.currentMode = mode;
30
+ if (mode === 'filters') {
31
+ this.applyFilterMode();
32
+ }
33
+ }
34
+
35
+ getLocalizedFilterName(locale, filterId) {
36
+ if (!locale) return filterId;
37
+ const languagePriority = ['en-US', 'en', 'ru'];
38
+
39
+ for (const lang of languagePriority) {
40
+ if (locale[lang]?.[filterId]) return locale[lang][filterId];
41
+ }
42
+
43
+ for (const lang in locale) {
44
+ if (locale[lang]?.[filterId]) return locale[lang][filterId];
45
+ }
46
+
47
+ return filterId;
48
+ }
49
+
50
+ applyFilterMode() {
51
+ if (this.isUpdating) return;
52
+ this.isUpdating = true;
53
+
54
+ try {
55
+ this.currentStyle?.metadata?.filters && Object.keys(this.currentStyle.metadata.filters).forEach(filterId => {
56
+ this.applyFilter(filterId, this.filterStates[filterId] || false);
57
+ });
58
+ } finally {
59
+ this.isUpdating = false;
60
+ }
61
+ }
62
+
63
+ applyFilter(filterId, isActive) {
64
+ if (this.currentMode !== 'filters' || !this.currentStyle?.metadata?.filters || this.isUpdating) return;
65
+
66
+ const filterConfig = this.currentStyle.metadata.filters[filterId];
67
+ if (!filterConfig) return;
68
+
69
+ const hasMapboxFilters = filterConfig.some(filter => filter.filter);
70
+ hasMapboxFilters ? this.applyLevel2Filter(filterId, isActive, filterConfig) : this.applyLevel1Filter(filterId, isActive, filterConfig);
71
+ }
72
+
73
+ applyLevel1Filter(filterId, isActive, filterConfig) {
74
+ this.currentStyle.layers.forEach(layer => {
75
+ if (!layer.metadata?.filter_id) return;
76
+
77
+ const matchingFilter = filterConfig.find(filter => filter.id === layer.metadata.filter_id);
78
+ if (matchingFilter) {
79
+ if (filterConfig.length > 1) {
80
+ const subFilterKey = `${filterId}_${layer.metadata.filter_id}`;
81
+ const subFilterActive = this.filterStates[subFilterKey];
82
+ const visibility = (isActive && subFilterActive) ? 'visible' : 'none';
83
+ this.map.getLayer(layer.id) && this.map.setLayoutProperty(layer.id, 'visibility', visibility);
84
+ } else {
85
+ this.map.getLayer(layer.id) && this.map.setLayoutProperty(layer.id, 'visibility', isActive ? 'visible' : 'none');
86
+ }
87
+ } else if (layer.metadata.filter_id === filterId) {
88
+ this.map.getLayer(layer.id) && this.map.setLayoutProperty(layer.id, 'visibility', isActive ? 'visible' : 'none');
89
+ }
90
+ });
91
+ }
92
+
93
+ applyLevel2Filter(filterId, isActive, filterConfig) {
94
+ const subFiltersWithExpr = filterConfig.filter(f => !!f.filter);
95
+ const subFiltersWithoutExpr = filterConfig.filter(f => !f.filter);
96
+
97
+ const generalLayers = this.currentStyle.layers.filter(layer => layer.metadata?.filter_id === filterId);
98
+ const childLayersBySubId = {};
99
+
100
+ filterConfig.forEach(f => {
101
+ childLayersBySubId[f.id] = this.currentStyle.layers
102
+ .filter(layer => layer.metadata?.filter_id === f.id)
103
+ .map(l => l.id);
104
+ });
105
+
106
+ const hasGeneralLayers = generalLayers.length > 0 && subFiltersWithExpr.length > 0;
107
+ const hasChildLayers = Object.values(childLayersBySubId).some(arr => arr?.length > 0);
108
+
109
+ if (!isActive) {
110
+ if (hasGeneralLayers) {
111
+ generalLayers.forEach(layer => {
112
+ if (this.map.getLayer(layer.id)) {
113
+ this.map.setLayoutProperty(layer.id, 'visibility', 'none');
114
+ this.map.setFilter(layer.id, null);
115
+ }
116
+ });
117
+ }
118
+ if (hasChildLayers) {
119
+ Object.values(childLayersBySubId).forEach(ids => {
120
+ ids.forEach(id => this.map.getLayer(id) && this.map.setLayoutProperty(id, 'visibility', 'none'));
121
+ });
122
+ }
123
+ return;
124
+ }
125
+
126
+ if (hasGeneralLayers) {
127
+ const activeWithExpr = subFiltersWithExpr.filter(f => this.filterStates[`${filterId}_${f.id}`] !== false);
128
+
129
+ if (activeWithExpr.length === 0) {
130
+ generalLayers.forEach(layer => {
131
+ if (this.map.getLayer(layer.id)) {
132
+ this.map.setLayoutProperty(layer.id, 'visibility', 'none');
133
+ this.map.setFilter(layer.id, null);
134
+ }
135
+ });
136
+ } else if (activeWithExpr.length === 1) {
137
+ const expr = activeWithExpr[0].filter;
138
+ generalLayers.forEach(layer => {
139
+ if (this.map.getLayer(layer.id)) {
140
+ this.map.setLayoutProperty(layer.id, 'visibility', 'visible');
141
+ this.map.setFilter(layer.id, expr);
142
+ }
143
+ });
144
+ } else {
145
+ const exprs = activeWithExpr.map(f => f.filter);
146
+ const combined = ['any', ...exprs];
147
+ generalLayers.forEach(layer => {
148
+ if (this.map.getLayer(layer.id)) {
149
+ this.map.setLayoutProperty(layer.id, 'visibility', 'visible');
150
+ this.map.setFilter(layer.id, combined);
151
+ }
152
+ });
153
+ }
154
+ }
155
+
156
+ if (hasChildLayers) {
157
+ filterConfig.forEach(sf => {
158
+ const ids = childLayersBySubId[sf.id] || [];
159
+ if (!ids.length) return;
160
+ const subKey = `${filterId}_${sf.id}`;
161
+ const on = (this.filterStates[subKey] !== false) && isActive;
162
+ ids.forEach(id => this.map.getLayer(id) && this.map.setLayoutProperty(id, 'visibility', on ? 'visible' : 'none'));
163
+ });
164
+ }
165
+ }
166
+
167
+ toggleAllFilters() {
168
+ if (this.currentMode !== 'filters' || !this.currentStyle?.metadata?.filters) return;
169
+
170
+ const allActive = Object.values(this.filterStates).every(state => state);
171
+ const newState = !allActive;
172
+
173
+ Object.keys(this.subFilterStatesBeforeGroupToggle).forEach(k => delete this.subFilterStatesBeforeGroupToggle[k]);
174
+ Object.keys(this.currentStyle.metadata.filters).forEach(filterId => {
175
+ const filterConfig = this.currentStyle.metadata.filters[filterId];
176
+
177
+ if (filterConfig?.length > 1) {
178
+ filterConfig.forEach(item => {
179
+ this.filterStates[`${filterId}_${item.id}`] = newState;
180
+ });
181
+ }
182
+
183
+ this.filterStates[filterId] = newState;
184
+ this.applyFilter(filterId, newState);
185
+ });
186
+
187
+ this.updateFilterButtons();
188
+ }
189
+
190
+ toggleFilterGroup(filterId) {
191
+ if (this.currentMode !== 'filters') return;
192
+
193
+ const filterConfig = this.currentStyle.metadata.filters[filterId];
194
+ const isCurrentlyActive = this.filterStates[filterId];
195
+
196
+ if (isCurrentlyActive) {
197
+ if (filterConfig?.length > 1) {
198
+ this.subFilterStatesBeforeGroupToggle[filterId] = {};
199
+ filterConfig.forEach(item => {
200
+ const subFilterKey = `${filterId}_${item.id}`;
201
+ this.subFilterStatesBeforeGroupToggle[filterId][item.id] = this.filterStates[subFilterKey];
202
+ this.filterStates[subFilterKey] = false;
203
+ });
204
+ }
205
+ this.filterStates[filterId] = false;
206
+ this.applyFilter(filterId, false);
207
+ } else {
208
+ if (filterConfig?.length > 1) {
209
+ const saved = this.subFilterStatesBeforeGroupToggle[filterId];
210
+ const anyUserSelectionWhileOff = filterConfig.some(item => this.filterStates[`${filterId}_${item.id}`]);
211
+ if (!anyUserSelectionWhileOff) {
212
+ filterConfig.forEach(item => {
213
+ const subFilterKey = `${filterId}_${item.id}`;
214
+ this.filterStates[subFilterKey] = saved?.[item.id] ?? true;
215
+ });
216
+ }
217
+ delete this.subFilterStatesBeforeGroupToggle[filterId];
218
+ }
219
+ this.filterStates[filterId] = true;
220
+ this.applyFilter(filterId, true);
221
+ }
222
+
223
+ this.updateFilterButtons();
224
+ }
225
+
226
+ toggleSubFilter(groupId, subFilterId) {
227
+ if (this.currentMode !== 'filters') return;
228
+ const subFilterKey = `${groupId}_${subFilterId}`;
229
+ this.filterStates[subFilterKey] = !this.filterStates[subFilterKey];
230
+
231
+ const filterConfig = this.currentStyle.metadata.filters[groupId];
232
+ if (filterConfig?.length > 1) {
233
+ const activeSubFilters = filterConfig.filter(item => this.filterStates[`${groupId}_${item.id}`]);
234
+ this.filterStates[groupId] = activeSubFilters.length > 0;
235
+ }
236
+
237
+ this.applyFilter(groupId, !!this.filterStates[groupId]);
238
+ this.updateFilterButtons();
239
+ }
240
+
241
+ updateFilterButtons() {
242
+ if (!this.currentStyle?.metadata?.filters) return;
243
+
244
+ Object.keys(this.currentStyle.metadata.filters).forEach(filterId => {
245
+ const groupButton = document.getElementById(`filter-${filterId}`);
246
+ groupButton && (groupButton.className = `control-button ${this.filterStates[filterId] ? 'active' : 'inactive'} filter-group-button`);
247
+
248
+ const subButton = document.getElementById(`filter-sub-${filterId}`);
249
+ subButton?.querySelectorAll('.filter-sub-button').forEach(btn => {
250
+ const subFilterId = btn.id.replace(`filter-sub-${filterId}-`, '');
251
+ const subFilterKey = `${filterId}_${subFilterId}`;
252
+ btn.className = `control-button ${this.filterStates[subFilterKey] ? 'active' : 'inactive'} filter-sub-button`;
253
+ });
254
+ });
255
+ }
256
+
257
+ createFilterButtons() {
258
+ if (!this.currentStyle?.metadata?.filters) return;
259
+
260
+ const filterButtonsContainer = document.getElementById('filter-buttons');
261
+ if (!filterButtonsContainer) return;
262
+
263
+ const savedStates = {...this.filterStates};
264
+
265
+ Object.keys(this.currentStyle.metadata.filters).forEach(filterId => {
266
+ const filterConfig = this.currentStyle.metadata.filters[filterId];
267
+ const locale = this.getLocalizedFilterName(this.currentStyle.metadata.locale, filterId);
268
+
269
+ const groupContainer = document.createElement('div');
270
+ groupContainer.className = 'filter-group';
271
+ groupContainer.id = `filter-group-${filterId}`;
272
+
273
+ const groupButton = document.createElement('button');
274
+ groupButton.id = `filter-${filterId}`;
275
+ groupButton.className = 'control-button active filter-group-button';
276
+ groupButton.textContent = locale;
277
+ groupButton.onclick = () => this.toggleFilterGroup(filterId);
278
+ groupContainer.appendChild(groupButton);
279
+
280
+ if (filterConfig.length > 1) {
281
+ const subButtonsContainer = document.createElement('div');
282
+ subButtonsContainer.className = 'filter-sub-buttons';
283
+ subButtonsContainer.id = `filter-sub-${filterId}`;
284
+
285
+ filterConfig.forEach(item => {
286
+ const subButton = document.createElement('button');
287
+ subButton.id = `filter-sub-${filterId}-${item.id}`;
288
+ subButton.className = 'control-button active filter-sub-button';
289
+ const subLocale = this.getLocalizedFilterName(this.currentStyle.metadata.locale, item.id);
290
+ subButton.textContent = subLocale || item.id;
291
+ subButton.onclick = () => this.toggleSubFilter(filterId, item.id);
292
+ subButtonsContainer.appendChild(subButton);
293
+
294
+ const subFilterKey = `${filterId}_${item.id}`;
295
+ const isLayerVisible = item.group_id ? true :
296
+ this.currentStyle.layers
297
+ .filter(layer => layer.metadata?.filter_id === item.id)
298
+ .some(layer => this.map.getLayoutProperty(layer.id, 'visibility') !== 'none');
299
+ this.filterStates[subFilterKey] = savedStates[subFilterKey] ?? isLayerVisible;
300
+ });
301
+
302
+ groupContainer.appendChild(subButtonsContainer);
303
+ }
304
+
305
+ filterButtonsContainer.appendChild(groupContainer);
306
+
307
+ const anyLayerVisible = filterConfig.length > 1 ?
308
+ filterConfig.some(item => this.filterStates[`${filterId}_${item.id}`] === true) :
309
+ this.currentStyle.layers
310
+ .filter(layer => layer.metadata?.filter_id === filterId)
311
+ .some(layer => this.map.getLayoutProperty(layer.id, 'visibility') !== 'none');
312
+ this.filterStates[filterId] = savedStates[filterId] ?? anyLayerVisible;
313
+ });
314
+
315
+ this.updateFilterButtons();
316
+ }
317
+
318
+ applyAllFilters() {
319
+ this.currentStyle?.metadata?.filters && Object.keys(this.currentStyle.metadata.filters).forEach(filterId => {
320
+ this.applyFilter(filterId, this.filterStates[filterId] || false);
321
+ });
322
+ }
323
+ }
@@ -1,3 +1,3 @@
1
1
  module MapLibrePreview
2
- VERSION = '0.0.2'
2
+ VERSION = '1.1.0'
3
3
  end
@@ -0,0 +1,329 @@
1
+ doctype html
2
+ html
3
+ head
4
+ meta[charset="utf-8"]
5
+ title Map Preview
6
+ meta[name="viewport" content="width=device-width, initial-scale=1.0"]
7
+ link[rel="icon" type="image/png" href="/icons/favicon.png"]
8
+ link[href="https://unpkg.com/maplibre-gl@#{MapLibrePreview::MAPLIBRE_VERSION}/dist/maplibre-gl.css" rel='stylesheet']
9
+ script[src="https://unpkg.com/maplibre-gl@#{MapLibrePreview::MAPLIBRE_VERSION}/dist/maplibre-gl.js"]
10
+ script[src="https://unpkg.com/maplibre-contour@#{MapLibrePreview::CONTOUR_VERSION}/dist/index.min.js"]
11
+ script[src="https://d3js.org/d3.v#{MapLibrePreview::D3_VERSION}.min.js"]
12
+ script[src='/js/filters.js']
13
+ script[src='/js/contour.js']
14
+ style
15
+ | * { margin: 0; padding: 0; box-sizing: border-box; }
16
+ | html { height: 100%; }
17
+ | body {
18
+ | font-family: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
19
+ | background: #2b2b2b; color: #a9b7c6; line-height: 1.4; font-size: 14px;
20
+ | margin: 0; padding: 0; overflow: hidden;
21
+ | }
22
+ | .container { width: 100%; height: 100vh; margin: 0; padding: 0; }
23
+ | .header {
24
+ | background: #3c3f41; border: 1px solid #555555; padding: 20px;
25
+ | border-radius: 4px; margin-bottom: 20px;
26
+ | }
27
+ | .header h1 { color: #ffc66d; font-size: 20px; margin-bottom: 8px; }
28
+ | .header p { color: #808080; font-size: 13px; }
29
+ | .section {
30
+ | background: #3c3f41; border: 1px solid #555555; padding: 16px;
31
+ | border-radius: 4px; margin-bottom: 16px;
32
+ | }
33
+ | .section-title {
34
+ | color: #6a9955; font-weight: bold; margin-bottom: 12px; font-size: 16px;
35
+ | }
36
+ | .status-ok { color: #6a9955; }
37
+ | .status-error { color: #f44747; }
38
+ | .status-warning { color: #ffc66d; }
39
+ | .status-disabled { color: #808080; }
40
+ | .style-card {
41
+ | background: #313335; border: 1px solid #464749; padding: 12px;
42
+ | border-radius: 3px; margin-bottom: 16px;
43
+ | }
44
+ | .style-header { display: flex; justify-content: space-between; margin-bottom: 8px; }
45
+ | .style-name { color: #ffc66d; font-weight: bold; }
46
+ | .style-id { color: #808080; font-size: 12px; }
47
+ | .style-description { color: #a9b7c6; font-size: 12px; margin-bottom: 8px; }
48
+ | .style-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px; }
49
+ | .stat-item { font-size: 12px; }
50
+ | .stat-label { color: #808080; }
51
+ | .stat-value { color: #6897bb; }
52
+ | .endpoint-container {
53
+ | display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
54
+ | padding: 2px 4px; border-radius: 2px; transition: all 0.2s ease; margin-left: 4px;
55
+ | }
56
+ | .endpoint-container:hover { background: #4b4d4f; }
57
+ | .endpoint-container.copy-success {
58
+ | background: #2d4a2d !important; border: 1px solid #6a9955;
59
+ | }
60
+ | .endpoint-url {
61
+ | color: #6897bb; font-family: 'Courier New', monospace; font-size: 11px;
62
+ | text-decoration: underline;
63
+ | }
64
+ | .copy-icon {
65
+ | color: #555555; font-size: 9px; flex-shrink: 0; transition: color 0.2s; opacity: 0.5;
66
+ | }
67
+ | .endpoint-container:hover .copy-icon { color: #808080; opacity: 0.8; }
68
+ | .copy-success .copy-icon { color: #6a9955 !important; opacity: 1 !important; }
69
+ | .style-actions { margin-top: 12px; }
70
+ | .btn {
71
+ | display: inline-block; padding: 6px 12px; background: #4b4d4f; color: #a9b7c6;
72
+ | text-decoration: none; border-radius: 3px; font-size: 12px;
73
+ | border: 1px solid #555555; margin-right: 8px;
74
+ | }
75
+ | .btn:hover { background: #5a5d5f; border-color: #666666; }
76
+ | .btn-primary {
77
+ | background: #6a9955; color: #ffffff; border-color: #7bb366;
78
+ | }
79
+ | .btn-primary:hover { background: #7bb366; border-color: #8cc477; }
80
+ | .btn-small { padding: 4px 8px; font-size: 10px; margin-right: 4px; }
81
+ | .refresh-btn {
82
+ | background: #ffc66d; color: #2b2b2b; border-color: #ffd87d; font-weight: bold;
83
+ | }
84
+ | .refresh-btn:hover { background: #ffd87d; border-color: #ffe88d; }
85
+ | .header-buttons { display: flex; gap: 8px; float: right; }
86
+ | .map-btn { background: #6897bb; color: #ffffff; border-color: #7aa8cc; }
87
+ | .map-btn:hover { background: #7aa8cc; border-color: #8bb9dd; }
88
+ | .sprite-section {
89
+ | margin-top: 12px; padding-top: 12px; border-top: 1px solid #464749;
90
+ | }
91
+ | .sprite-title { color: #6a9955; font-weight: bold; font-size: 12px; margin-bottom: 8px; }
92
+ | .sprite-endpoints { display: flex; flex-direction: column; gap: 6px; }
93
+ | .sprite-item { display: flex; align-items: center; gap: 8px; font-size: 11px; }
94
+ | .sprite-label { color: #808080; min-width: 40px; }
95
+ | .stats-grid {
96
+ | display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
97
+ | gap: 16px; margin-bottom: 20px;
98
+ | }
99
+ | .stat-box {
100
+ | background: #313335; border: 1px solid #464749; padding: 12px;
101
+ | border-radius: 3px; text-align: center;
102
+ | }
103
+ | .stat-number { font-size: 24px; font-weight: bold; color: #6897bb; margin-bottom: 4px; }
104
+ | .stat-desc { font-size: 12px; color: #808080; }
105
+ | .error-message { color: #f44747; font-size: 12px; margin-top: 4px; }
106
+ | .success-message { color: #6a9955; font-size: 12px; margin-top: 4px; }
107
+ | .performance-overlay {
108
+ | position: fixed; top: 0; left: 50%; transform: translateX(-50%); z-index: 1000;
109
+ | background: rgba(43, 43, 43, 0.95); border: 1px solid #555555;
110
+ | border-radius: 0 0 6px 6px; padding: 0; min-width: 400px;
111
+ | backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
112
+ | transition: all 0.3s ease;
113
+ | }
114
+ | .performance-toggle {
115
+ | position: absolute; top: 4px; right: 4px; z-index: 1001;
116
+ | background: none; border: none; color: #808080; cursor: pointer;
117
+ | font-size: 14px; padding: 0; width: 16px; height: 16px;
118
+ | display: flex; align-items: center; justify-content: center;
119
+ | border-radius: 2px; transition: all 0.2s ease;
120
+ | }
121
+ | .performance-toggle:hover {
122
+ | background: #4b4d4f; color: #a9b7c6;
123
+ | }
124
+ | .performance-content {
125
+ | padding: 8px 12px;
126
+ | }
127
+ | .metric-row {
128
+ | display: flex; justify-content: space-between; align-items: center;
129
+ | margin-bottom: 6px;
130
+ | }
131
+ | .metric-row:last-child { margin-bottom: 0; }
132
+ | .metric {
133
+ | display: flex; align-items: center; gap: 4px;
134
+ | font-size: 11px; flex: 1; justify-content: center;
135
+ | }
136
+ | .metric-label {
137
+ | color: #808080; font-weight: 500;
138
+ | }
139
+ | .metric-value {
140
+ | color: #6897bb; font-weight: bold; font-family: 'Courier New', monospace;
141
+ | min-width: 40px; text-align: center;
142
+ | }
143
+ | .metric-value.warning { color: #ffc66d; }
144
+ | .metric-value.error { color: #f44747; }
145
+ | .metric-value.success { color: #6a9955; }
146
+ | #map-container {
147
+ | position: relative; width: 100%; height: 100vh; background: #2b2b2b;
148
+ | }
149
+ | .map-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
150
+ | .controls {
151
+ | position: absolute; top: 10px; left: 10px; z-index: 1000;
152
+ | background: rgba(60, 63, 65, 0.95); border: 1px solid #555555;
153
+ | padding: 10px; border-radius: 4px; color: #a9b7c6;
154
+ | }
155
+ | .controls a { color: #6897bb; text-decoration: none; margin-right: 15px; }
156
+ | .controls a:hover { text-decoration: underline; }
157
+ | .layer-controls {
158
+ | position: absolute; top: 50%; left: 10px; transform: translateY(-50%); z-index: 1000;
159
+ | background: rgba(60, 63, 65, 0.95); border: 1px solid #555555; padding: 15px;
160
+ | border-radius: 4px; color: #a9b7c6; display: flex; flex-direction: column; gap: 6px;
161
+ | max-width: 300px; max-height: 80vh;
162
+ | }
163
+ | .mode-switcher {
164
+ | display: flex; gap: 2px; margin-bottom: 10px; background: #3c3f41;
165
+ | border-radius: 3px; padding: 2px; flex-shrink: 0;
166
+ | }
167
+ | .mode-button {
168
+ | background: #3c3f41; color: #808080; border: none; padding: 6px 12px;
169
+ | border-radius: 2px; cursor: pointer; font-size: 11px; transition: all 0.2s ease; flex: 1;
170
+ | }
171
+ | .mode-button:hover { background: #4b4d4f; color: #a9b7c6; }
172
+ | .mode-button.active { background: #6897bb; color: #ffffff; }
173
+ | .control-panel {
174
+ | display: none; flex-direction: column; gap: 6px;
175
+ | max-height: calc(80vh - 200px); overflow-y: auto;
176
+ | }
177
+ | .control-panel::-webkit-scrollbar { width: 8px; }
178
+ | .control-panel::-webkit-scrollbar-track { background: #3c3f41; border-radius: 4px; }
179
+ | .control-panel::-webkit-scrollbar-thumb {
180
+ | background: #555555; border-radius: 4px; border: 1px solid #3c3f41;
181
+ | }
182
+ | .control-panel::-webkit-scrollbar-thumb:hover { background: #666666; }
183
+ | .control-panel { scrollbar-width: thin; scrollbar-color: #555555 #3c3f41; }
184
+ | .control-panel.active { display: flex; }
185
+ | .control-panel-header {
186
+ | flex-shrink: 0; border-bottom: 1px solid #555555; padding-bottom: 8px; margin-bottom: 8px;
187
+ | display: flex; flex-direction: column; gap: 6px;
188
+ | }
189
+ | .control-panel-content {
190
+ | flex: 1; overflow-y: auto;
191
+ | }
192
+ | .control-button {
193
+ | background: #4b4d4f; color: #a9b7c6; border: 1px solid #555555;
194
+ | padding: 6px 12px; border-radius: 3px; cursor: pointer; font-size: 11px;
195
+ | transition: all 0.2s ease; width: 100%; box-sizing: border-box;
196
+ | }
197
+ | .control-button:hover { background: #5a5d5f; border-color: #666666; }
198
+ | .control-button.active { background: #6a9955; color: #ffffff; border-color: #7bb366; }
199
+ | .control-button.inactive { background: #3c3f41; color: #808080; border-color: #464749; }
200
+ | .filter-group {
201
+ | margin-bottom: 8px;
202
+ | border: 1px solid #555555;
203
+ | border-radius: 3px;
204
+ | overflow: hidden;
205
+ | }
206
+ | .filter-group-button {
207
+ | width: 100%;
208
+ | font-weight: bold;
209
+ | background: #4b4d4f;
210
+ | border: none;
211
+ | border-bottom: 1px solid #555555;
212
+ | }
213
+ | .filter-sub-buttons {
214
+ | display: flex;
215
+ | flex-direction: column;
216
+ | gap: 2px;
217
+ | padding: 3px;
218
+ | background: #3c3f41;
219
+ | border: 1px solid #6a9955;
220
+ | border-radius: 2px;
221
+ | box-sizing: border-box;
222
+ | }
223
+ | .filter-sub-button {
224
+ | font-size: 10px;
225
+ | padding: 4px 8px;
226
+ | margin: 0;
227
+ | background: #ffc66d;
228
+ | color: #2b2b2b;
229
+ | border: 1px solid #e6b85a;
230
+ | border-radius: 2px;
231
+ | cursor: pointer;
232
+ | transition: all 0.2s ease;
233
+ | }
234
+ | .filter-sub-button:hover {
235
+ | background: #ffd87d;
236
+ | border-color: #f0c050;
237
+ | }
238
+ | .filter-sub-button.active {
239
+ | background: #ffd87d;
240
+ | color: #2b2b2b;
241
+ | border-color: #f0c050;
242
+ | font-weight: bold;
243
+ | }
244
+ | .filter-sub-button.inactive {
245
+ | background: #3c3f41;
246
+ | color: #808080;
247
+ | border-color: #464749;
248
+ | }
249
+
250
+ | .toggle-basemap {
251
+ | position: absolute; top: 10px; right: 10px; z-index: 1000;
252
+ | background: rgba(60, 63, 65, 0.95); border: 1px solid #555555; padding: 10px;
253
+ | border-radius: 4px; color: #a9b7c6; border: none; cursor: pointer;
254
+ | }
255
+ | .toggle-basemap:hover { background: rgba(75, 77, 79, 0.95); }
256
+ | #loading-indicator {
257
+ | position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
258
+ | background: rgba(60, 63, 65, 0.95); border: 1px solid #555555;
259
+ | border-radius: 6px; padding: 12px 20px; z-index: 1000;
260
+ | backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
261
+ | transition: all 0.3s ease; display: none;
262
+ | }
263
+ | .loading-progress { text-align: center; color: #a9b7c6; }
264
+ | .loading-text { font-size: 14px; color: #a9b7c6; margin-bottom: 8px; }
265
+ | .progress-bar {
266
+ | width: 200px; height: 4px; background: #3c3f41; border-radius: 2px;
267
+ | overflow: hidden; margin: 0 auto;
268
+ | }
269
+ | .progress-fill {
270
+ | height: 100%; background: #6897bb; border-radius: 2px;
271
+ | transition: width 0.3s ease; width: 0%;
272
+ | }
273
+ | /* MapLibre Controls */
274
+ | .maplibregl-ctrl-scale, .maplibregl-ctrl-group, .maplibregl-ctrl-terrain, .maplibregl-ctrl-globe {
275
+ | background: rgba(60, 63, 65, 0.95) !important; border: 1px solid #555555 !important;
276
+ | border-radius: 4px !important; backdrop-filter: blur(10px) !important;
277
+ | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
278
+ | }
279
+ | .maplibregl-ctrl-scale { color: rgb(100, 185, 61) !important; font-size: 11px !important; padding: 4px 8px !important; }
280
+ | .maplibregl-ctrl-scale-line { background: #6897bb !important; border: 1px solid #7aa8cc !important; }
281
+ | .maplibregl-ctrl-group button { background: rgba(75, 105, 61, 0.8) !important; border: 1px solid rgba(72, 139, 92, 0.4) !important; color: #ffffff !important; font-size: 14px !important; font-weight: bold !important; transition: all 0.2s ease !important; }
282
+ | .maplibregl-ctrl-group button:hover { background: rgba(255, 198, 109, 0.9) !important; border-color: #ffd87d !important; color: #2b2b2b !important; transform: scale(1.05) !important; }
283
+ | .maplibregl-ctrl-group button:active { background: rgba(106, 153, 85, 1) !important; transform: scale(0.95) !important; }
284
+ | .maplibregl-ctrl-terrain button { background: rgba(104, 151, 187, 0.8) !important; border: 1px solid #7aa8cc !important; color: #ffffff !important; font-size: 16px !important; font-weight: bold !important; transition: all 0.2s ease !important; padding: 8px !important; }
285
+ | .maplibregl-ctrl-terrain button:hover { background: rgba(104, 151, 187, 1) !important; border-color: #8bb9dd !important; transform: scale(1.1) !important; box-shadow: 0 2px 8px rgba(104, 151, 187, 0.4) !important; }
286
+ | .maplibregl-ctrl-terrain button:active { transform: scale(0.95) !important; }
287
+ | .maplibregl-ctrl-globe button { background: rgba(255, 198, 109, 0.8) !important; border: 1px solid #ffd87d !important; color: #2b2b2b !important; font-size: 16px !important; font-weight: bold !important; transition: all 0.2s ease !important; padding: 8px !important; }
288
+ | .maplibregl-ctrl-globe button:hover { background: rgba(255, 198, 109, 1) !important; border-color: #ffe88d !important; transform: scale(1.1) !important; box-shadow: 0 2px 8px rgba(255, 198, 109, 0.4) !important; }
289
+ | .maplibregl-ctrl-globe button:active { transform: scale(0.95) !important; }
290
+ | .profile-overlay {
291
+ | position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 1000;
292
+ | background: rgba(60, 63, 65, 0.95); border: 1px solid #555555;
293
+ | border-radius: 6px; padding: 12px; width: 70vw; max-width: 800px;
294
+ | backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
295
+ | }
296
+ | .profile-header {
297
+ | display: flex; justify-content: space-between; align-items: center;
298
+ | margin-bottom: 8px; border-bottom: 1px solid #555555; padding-bottom: 8px;
299
+ | }
300
+ | .profile-title { color: #ffc66d; font-weight: bold; font-size: 12px; }
301
+ | .profile-close {
302
+ | background: none; border: none; color: #808080; cursor: pointer;
303
+ | font-size: 14px; padding: 0; width: 16px; height: 16px;
304
+ | display: flex; align-items: center; justify-content: center;
305
+ | border-radius: 2px; transition: all 0.2s ease;
306
+ | }
307
+ | .profile-close:hover { background: #4b4d4f; color: #a9b7c6; }
308
+ | .profile-stats {
309
+ | display: flex; justify-content: space-between; margin-bottom: 8px;
310
+ | font-size: 10px; color: #808080;
311
+ | }
312
+ | .profile-chart { margin-top: 8px; }
313
+ | .elevation-tooltip {
314
+ | position: fixed; z-index: 1001; pointer-events: none;
315
+ | background: rgba(60, 63, 65, 0.95); border: 1px solid #555555;
316
+ | border-radius: 4px; padding: 8px 12px; font-size: 16px;
317
+ | color: #6a9955; backdrop-filter: blur(10px);
318
+ | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
319
+ | transform: translate(10px, -50%); margin-left: 10px;
320
+ | font-weight: bold;
321
+ | }
322
+ | .profile-area { fill: #4CAF50; opacity: 0.3; }
323
+ | .profile-line { fill: none; stroke: #2196F3; stroke-width: 2; }
324
+ | .profile-tooltip-line { stroke: #ff0000; stroke-width: 1; stroke-dasharray: 3,3; }
325
+ | .profile-tooltip-circle { fill: #ff0000; r: 4; }
326
+ | .profile-tooltip-text { text-anchor: middle; fill: #ff0000; font-size: 14px; }
327
+ body
328
+ .container
329
+ == yield