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.
@@ -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.1'
2
+ VERSION = '1.0.0'
3
3
  end