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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +16 -0
- data/.rspec +3 -0
- data/.rubocop.yml +260 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +51 -0
- data/LICENSE +21 -0
- data/README.md +312 -0
- data/Rakefile +50 -0
- data/docs/README_RU.md +312 -0
- data/lib/maplibre-preview/public/js/contour.js +85 -0
- data/lib/maplibre-preview/public/js/filters.js +323 -0
- data/lib/maplibre-preview/version.rb +1 -1
- data/lib/maplibre-preview/views/maplibre_layout.slim +329 -0
- data/lib/maplibre-preview/views/maplibre_map.slim +829 -0
- data/lib/maplibre-preview.rb +77 -2
- data/spec/maplibre_preview_spec.rb +52 -0
- data/spec/spec_helper.rb +11 -0
- metadata +88 -35
- data/lib/maplibre-preview/sinatra_ext.rb +0 -27
- data/lib/maplibre-preview/views/maplibre_preview.slim +0 -3
@@ -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
|
+
}
|
@@ -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
|