maplibre-preview 0.0.1 → 1.0.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 +35 -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/map.slim +829 -0
- data/lib/maplibre-preview/views/map_layout.slim +329 -0
- data/lib/maplibre-preview.rb +113 -2
- data/spec/maplibre_preview_spec.rb +52 -0
- data/spec/spec_helper.rb +11 -0
- metadata +89 -36
- 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
|
+
}
|