001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.Graphics2D;
008import java.util.ArrayList;
009import java.util.Collection;
010import java.util.Collections;
011import java.util.HashSet;
012import java.util.LinkedList;
013import java.util.List;
014import java.util.Set;
015import java.util.Stack;
016import java.util.stream.Collectors;
017
018import javax.swing.JOptionPane;
019
020import org.openstreetmap.josm.data.SortableModel;
021import org.openstreetmap.josm.data.StructUtils;
022import org.openstreetmap.josm.data.osm.Filter.FilterPreferenceEntry;
023import org.openstreetmap.josm.data.osm.search.SearchParseError;
024import org.openstreetmap.josm.gui.MainApplication;
025import org.openstreetmap.josm.gui.widgets.OSDLabel;
026import org.openstreetmap.josm.spi.preferences.Config;
027import org.openstreetmap.josm.tools.Logging;
028import org.openstreetmap.josm.tools.Utils;
029
030/**
031 * The model that is used both for auto and manual filters.
032 * @since 12400
033 */
034public class FilterModel implements SortableModel<Filter> {
035
036    /**
037     * number of primitives that are disabled but not hidden
038     */
039    private int disabledCount;
040    /**
041     * number of primitives that are disabled and hidden
042     */
043    private int disabledAndHiddenCount;
044    /**
045     * true, if the filter state (normal / disabled / hidden) of any primitive has changed in the process
046     */
047    private boolean changed;
048
049    private final List<Filter> filters = new LinkedList<>();
050    private final FilterMatcher filterMatcher = new FilterMatcher();
051
052    private void updateFilterMatcher() {
053        filterMatcher.reset();
054        for (Filter filter : filters) {
055            try {
056                filterMatcher.add(filter);
057            } catch (SearchParseError e) {
058                Logging.error(e);
059                JOptionPane.showMessageDialog(
060                        MainApplication.getMainFrame(),
061                        tr("<html>Error in filter <code>{0}</code>:<br>{1}",
062                                Utils.escapeReservedCharactersHTML(Utils.shortenString(filter.text, 80)),
063                                Utils.escapeReservedCharactersHTML(e.getMessage())),
064                        tr("Error in filter"),
065                        JOptionPane.ERROR_MESSAGE);
066                filter.enable = false;
067            }
068        }
069    }
070
071    /**
072     * Initializes the model from preferences.
073     * @param prefEntry preference key
074     */
075    public void loadPrefs(String prefEntry) {
076        List<FilterPreferenceEntry> entries = StructUtils.getListOfStructs(
077                Config.getPref(), prefEntry, null, FilterPreferenceEntry.class);
078        if (entries != null) {
079            for (FilterPreferenceEntry e : entries) {
080                filters.add(new Filter(e));
081            }
082            updateFilterMatcher();
083        }
084    }
085
086    /**
087     * Saves the model to preferences.
088     * @param prefEntry preferences key
089     */
090    public void savePrefs(String prefEntry) {
091        Collection<FilterPreferenceEntry> entries = filters.stream()
092                .map(Filter::getPreferenceEntry)
093                .collect(Collectors.toList());
094        StructUtils.putListOfStructs(Config.getPref(), prefEntry, entries, FilterPreferenceEntry.class);
095    }
096
097    /**
098     * Runs the filters on the current edit data set.
099     */
100    public void executeFilters() {
101        DataSet ds = OsmDataManager.getInstance().getActiveDataSet();
102        changed = false;
103        if (ds == null) {
104            disabledAndHiddenCount = 0;
105            disabledCount = 0;
106            changed = true;
107        } else {
108            final Collection<OsmPrimitive> deselect = new HashSet<>();
109
110            ds.beginUpdate();
111            try {
112                final Collection<OsmPrimitive> all = ds.allNonDeletedCompletePrimitives();
113
114                changed = FilterWorker.executeFilters(all, filterMatcher);
115
116                disabledCount = 0;
117                disabledAndHiddenCount = 0;
118                // collect disabled and selected the primitives
119                for (OsmPrimitive osm : all) {
120                    if (osm.isDisabled()) {
121                        disabledCount++;
122                        if (osm.isSelected()) {
123                            deselect.add(osm);
124                        }
125                        if (osm.isDisabledAndHidden()) {
126                            disabledAndHiddenCount++;
127                        }
128                    }
129                }
130                disabledCount -= disabledAndHiddenCount;
131            } finally {
132                if (changed) {
133                    ds.fireFilterChanged();
134                }
135                ds.endUpdate();
136            }
137
138            if (!deselect.isEmpty()) {
139                ds.clearSelection(deselect);
140            }
141        }
142        if (changed) {
143            updateMap();
144        }
145    }
146
147    /**
148     * Runs the filter on a list of primitives that are part of the edit data set.
149     * @param primitives The primitives
150     */
151    public void executeFilters(Collection<? extends OsmPrimitive> primitives) {
152        DataSet ds = OsmDataManager.getInstance().getActiveDataSet();
153        if (ds == null)
154            return;
155
156        changed = false;
157        List<OsmPrimitive> deselect = new ArrayList<>();
158
159        ds.update(() -> {
160            for (int i = 0; i < 2; i++) {
161                for (OsmPrimitive primitive: primitives) {
162
163                    if (i == 0 && primitive instanceof Node) {
164                        continue;
165                    }
166
167                    if (i == 1 && !(primitive instanceof Node)) {
168                        continue;
169                    }
170
171                    if (primitive.isDisabled()) {
172                        disabledCount--;
173                    }
174                    if (primitive.isDisabledAndHidden()) {
175                        disabledAndHiddenCount--;
176                    }
177                    changed |= FilterWorker.executeFilters(primitive, filterMatcher);
178                    if (primitive.isDisabled()) {
179                        disabledCount++;
180                    }
181                    if (primitive.isDisabledAndHidden()) {
182                        disabledAndHiddenCount++;
183                    }
184
185                    if (primitive.isSelected() && primitive.isDisabled()) {
186                        deselect.add(primitive);
187                    }
188                }
189            }
190        });
191
192        if (!deselect.isEmpty()) {
193            ds.clearSelection(deselect);
194        }
195        if (changed) {
196            updateMap();
197        }
198    }
199
200    private static void updateMap() {
201        MainApplication.getLayerManager().invalidateEditLayer();
202    }
203
204    /**
205     * Clears all filtered flags from all primitives in the dataset
206     */
207    public void clearFilterFlags() {
208        DataSet ds = OsmDataManager.getInstance().getActiveDataSet();
209        if (ds != null) {
210            FilterWorker.clearFilterFlags(ds.allPrimitives());
211        }
212        disabledCount = 0;
213        disabledAndHiddenCount = 0;
214    }
215
216    /**
217     * Removes all filters from this model.
218     */
219    public void clearFilters() {
220        filters.clear();
221        updateFilterMatcher();
222    }
223
224    /**
225     * Adds a new filter to the filter list.
226     * @param filter The new filter
227     * @return true (as specified by {@link Collection#add})
228     */
229    public boolean addFilter(Filter filter) {
230        filters.add(filter);
231        updateFilterMatcher();
232        return true;
233    }
234
235    /**
236     * Moves the filters in the given rows by a number of positions.
237     * @param delta negative or positive increment
238     * @param rowIndexes The filter rows
239     * @return true if the filters have been moved down
240     * @since 15226
241     */
242    public boolean moveFilters(int delta, int... rowIndexes) {
243        if (!canMove(delta, filters::size, rowIndexes))
244            return false;
245        doMove(delta, rowIndexes);
246        updateFilterMatcher();
247        return true;
248    }
249
250    /**
251     * Moves down the filter in the given row.
252     * @param rowIndex The filter row
253     * @return true if the filter has been moved down
254     */
255    public boolean moveDownFilter(int rowIndex) {
256        return moveFilters(1, rowIndex);
257    }
258
259    /**
260     * Moves up the filter in the given row
261     * @param rowIndex The filter row
262     * @return true if the filter has been moved up
263     */
264    public boolean moveUpFilter(int rowIndex) {
265        return moveFilters(-1, rowIndex);
266    }
267
268    /**
269     * Removes the filter that is displayed in the given row
270     * @param rowIndex The index of the filter to remove
271     * @return the filter previously at the specified position
272     */
273    public Filter removeFilter(int rowIndex) {
274        Filter result = filters.remove(rowIndex);
275        updateFilterMatcher();
276        return result;
277    }
278
279    @Override
280    public Filter setValue(int rowIndex, Filter filter) {
281        Filter result = filters.set(rowIndex, filter);
282        updateFilterMatcher();
283        return result;
284    }
285
286    @Override
287    public Filter getValue(int rowIndex) {
288        return filters.get(rowIndex);
289    }
290
291    /**
292     * Draws a text on the map display that indicates that filters are active.
293     * @param g The graphics to draw that text on.
294     * @param lblOSD On Screen Display label
295     * @param header The title to display at the beginning of OSD
296     * @param footer The message to display at the bottom of OSD. Must end by {@code </html>}
297     */
298    public void drawOSDText(Graphics2D g, OSDLabel lblOSD, String header, String footer) {
299        if (disabledCount == 0 && disabledAndHiddenCount == 0)
300            return;
301
302        String message = "<html>" + header;
303
304        if (disabledAndHiddenCount != 0) {
305            /* for correct i18n of plural forms - see #9110 */
306            message += trn("<p><b>{0}</b> object hidden", "<p><b>{0}</b> objects hidden", disabledAndHiddenCount, disabledAndHiddenCount);
307        }
308
309        if (disabledAndHiddenCount != 0 && disabledCount != 0) {
310            message += "<br>";
311        }
312
313        if (disabledCount != 0) {
314            /* for correct i18n of plural forms - see #9110 */
315            message += trn("<b>{0}</b> object disabled", "<b>{0}</b> objects disabled", disabledCount, disabledCount);
316        }
317
318        message += footer;
319
320        lblOSD.setText(message);
321        lblOSD.setSize(lblOSD.getPreferredSize());
322
323        int dx = MainApplication.getMap().mapView.getWidth() - lblOSD.getPreferredSize().width - 15;
324        int dy = 15;
325        g.translate(dx, dy);
326        lblOSD.paintComponent(g);
327        g.translate(-dx, -dy);
328    }
329
330    /**
331     * Returns the list of filters.
332     * @return the list of filters
333     */
334    public List<Filter> getFilters() {
335        return new ArrayList<>(filters);
336    }
337
338    /**
339     * Returns the number of filters.
340     * @return the number of filters
341     */
342    public int getFiltersCount() {
343        return filters.size();
344    }
345
346    /**
347     * Returns the number of primitives that are disabled but not hidden.
348     * @return the number of primitives that are disabled but not hidden
349     */
350    public int getDisabledCount() {
351        return disabledCount;
352    }
353
354    /**
355     * Returns the number of primitives that are disabled and hidden.
356     * @return the number of primitives that are disabled and hidden
357     */
358    public int getDisabledAndHiddenCount() {
359        return disabledAndHiddenCount;
360    }
361
362    /**
363     * Determines if the filter state (normal / disabled / hidden) of any primitive has changed in the process.
364     * @return true, if the filter state (normal / disabled / hidden) of any primitive has changed in the process
365     */
366    public boolean isChanged() {
367        return changed;
368    }
369
370    /**
371     * Determines if at least one filter is enabled.
372     * @return {@code true} if at least one filter is enabled
373     * @since 14206
374     */
375    public boolean hasFilters() {
376        return filterMatcher.hasFilters();
377    }
378
379    /**
380     * Returns the list of primitives whose filtering can be affected by change in primitive
381     * @param primitives list of primitives to check
382     * @return List of primitives whose filtering can be affected by change in source primitives
383     */
384    public static Collection<OsmPrimitive> getAffectedPrimitives(Collection<? extends OsmPrimitive> primitives) {
385        // Filters can use nested parent/child expression so complete tree is necessary
386        Set<OsmPrimitive> result = new HashSet<>();
387        Stack<OsmPrimitive> stack = new Stack<>();
388        stack.addAll(primitives);
389
390        while (!stack.isEmpty()) {
391            OsmPrimitive p = stack.pop();
392
393            if (result.contains(p)) {
394                continue;
395            }
396
397            result.add(p);
398
399            if (p instanceof Way) {
400                for (OsmPrimitive n: ((Way) p).getNodes()) {
401                    stack.push(n);
402                }
403            } else if (p instanceof Relation) {
404                for (RelationMember rm: ((Relation) p).getMembers()) {
405                    stack.push(rm.getMember());
406                }
407            }
408
409            for (OsmPrimitive ref: p.getReferrers()) {
410                stack.push(ref);
411            }
412        }
413
414        return result;
415    }
416
417    @Override
418    public void sort() {
419        Collections.sort(filters);
420        updateFilterMatcher();
421    }
422
423    @Override
424    public void reverse() {
425        Collections.reverse(filters);
426        updateFilterMatcher();
427    }
428}