001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trc;
006import static org.openstreetmap.josm.tools.I18n.trn;
007
008import java.awt.Color;
009import java.awt.Font;
010import java.awt.Graphics;
011import java.awt.Graphics2D;
012import java.util.ArrayList;
013import java.util.Collection;
014import java.util.HashSet;
015import java.util.LinkedList;
016import java.util.List;
017
018import javax.swing.BorderFactory;
019import javax.swing.JLabel;
020import javax.swing.JOptionPane;
021import javax.swing.table.AbstractTableModel;
022
023import org.openstreetmap.josm.Main;
024import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
025import org.openstreetmap.josm.data.osm.DataSet;
026import org.openstreetmap.josm.data.osm.Filter;
027import org.openstreetmap.josm.data.osm.Filter.FilterPreferenceEntry;
028import org.openstreetmap.josm.data.osm.FilterMatcher;
029import org.openstreetmap.josm.data.osm.FilterWorker;
030import org.openstreetmap.josm.data.osm.Node;
031import org.openstreetmap.josm.data.osm.OsmPrimitive;
032import org.openstreetmap.josm.tools.Utils;
033
034/**
035 *
036 * @author Petr_DlouhĂ˝
037 */
038public class FilterTableModel extends AbstractTableModel {
039
040    public static final int COL_ENABLED = 0;
041    public static final int COL_HIDING = 1;
042    public static final int COL_TEXT = 2;
043    public static final int COL_INVERTED = 3;
044
045    // number of primitives that are disabled but not hidden
046    public int disabledCount;
047    // number of primitives that are disabled and hidden
048    public int disabledAndHiddenCount;
049
050    /**
051     * Constructs a new {@code FilterTableModel}.
052     */
053    public FilterTableModel() {
054        loadPrefs();
055    }
056
057    private final transient List<Filter> filters = new LinkedList<>();
058    private final transient FilterMatcher filterMatcher = new FilterMatcher();
059
060    private void updateFilters() {
061        filterMatcher.reset();
062        for (Filter filter : filters) {
063            try {
064                filterMatcher.add(filter);
065            } catch (ParseError e) {
066                JOptionPane.showMessageDialog(
067                        Main.parent,
068                        tr("<html>Error in filter <code>{0}</code>:<br>{1}", Utils.shortenString(filter.text, 80), e.getMessage()),
069                        tr("Error in filter"),
070                        JOptionPane.ERROR_MESSAGE);
071                filter.enable = false;
072                savePrefs();
073            }
074        }
075        executeFilters();
076    }
077
078    public void executeFilters() {
079        DataSet ds = Main.main.getCurrentDataSet();
080        boolean changed = false;
081        if (ds == null) {
082            disabledAndHiddenCount = 0;
083            disabledCount = 0;
084            changed = true;
085        } else {
086            final Collection<OsmPrimitive> deselect = new HashSet<>();
087
088            ds.beginUpdate();
089            try {
090
091                final Collection<OsmPrimitive> all = ds.allNonDeletedCompletePrimitives();
092
093                changed = FilterWorker.executeFilters(all, filterMatcher);
094
095                disabledCount = 0;
096                disabledAndHiddenCount = 0;
097                // collect disabled and selected the primitives
098                for (OsmPrimitive osm : all) {
099                    if (osm.isDisabled()) {
100                        disabledCount++;
101                        if (osm.isSelected()) {
102                            deselect.add(osm);
103                        }
104                        if (osm.isDisabledAndHidden()) {
105                            disabledAndHiddenCount++;
106                        }
107                    }
108                }
109                disabledCount -= disabledAndHiddenCount;
110            } finally {
111                ds.endUpdate();
112            }
113
114            if (!deselect.isEmpty()) {
115                ds.clearSelection(deselect);
116            }
117        }
118
119        if (Main.isDisplayingMapView() && changed) {
120            Main.map.mapView.repaint();
121            Main.map.filterDialog.updateDialogHeader();
122        }
123    }
124
125    public void executeFilters(Collection<? extends OsmPrimitive> primitives) {
126        DataSet ds = Main.main.getCurrentDataSet();
127        if (ds == null)
128            return;
129
130        boolean changed = false;
131        List<OsmPrimitive> deselect = new ArrayList<>();
132
133        ds.beginUpdate();
134        try {
135            for (int i = 0; i < 2; i++) {
136                for (OsmPrimitive primitive: primitives) {
137
138                    if (i == 0 && primitive instanceof Node) {
139                        continue;
140                    }
141
142                    if (i == 1 && !(primitive instanceof Node)) {
143                        continue;
144                    }
145
146                    if (primitive.isDisabled()) {
147                        disabledCount--;
148                    }
149                    if (primitive.isDisabledAndHidden()) {
150                        disabledAndHiddenCount--;
151                    }
152                    changed = changed | FilterWorker.executeFilters(primitive, filterMatcher);
153                    if (primitive.isDisabled()) {
154                        disabledCount++;
155                    }
156                    if (primitive.isDisabledAndHidden()) {
157                        disabledAndHiddenCount++;
158                    }
159
160                    if (primitive.isSelected() && primitive.isDisabled()) {
161                        deselect.add(primitive);
162                    }
163
164                }
165            }
166        } finally {
167            ds.endUpdate();
168        }
169
170        if (changed) {
171            Main.map.mapView.repaint();
172            Main.map.filterDialog.updateDialogHeader();
173            ds.clearSelection(deselect);
174        }
175
176    }
177
178    public void clearFilterFlags() {
179        DataSet ds = Main.main.getCurrentDataSet();
180        if (ds != null) {
181            FilterWorker.clearFilterFlags(ds.allPrimitives());
182        }
183        disabledCount = 0;
184        disabledAndHiddenCount = 0;
185    }
186
187    private void loadPrefs() {
188        List<FilterPreferenceEntry> entries = Main.pref.getListOfStructs("filters.entries", null, FilterPreferenceEntry.class);
189        if (entries != null) {
190            for (FilterPreferenceEntry e : entries) {
191                filters.add(new Filter(e));
192            }
193            updateFilters();
194        }
195    }
196
197    private void savePrefs() {
198        Collection<FilterPreferenceEntry> entries = new ArrayList<>();
199        for (Filter flt : filters) {
200            entries.add(flt.getPreferenceEntry());
201        }
202        Main.pref.putListOfStructs("filters.entries", entries, FilterPreferenceEntry.class);
203    }
204
205    public void addFilter(Filter f) {
206        filters.add(f);
207        savePrefs();
208        updateFilters();
209        fireTableRowsInserted(filters.size() - 1, filters.size() - 1);
210    }
211
212    public void moveDownFilter(int i) {
213        if (i >= filters.size() - 1)
214            return;
215        filters.add(i + 1, filters.remove(i));
216        savePrefs();
217        updateFilters();
218        fireTableRowsUpdated(i, i + 1);
219    }
220
221    public void moveUpFilter(int i) {
222        if (i == 0)
223            return;
224        filters.add(i - 1, filters.remove(i));
225        savePrefs();
226        updateFilters();
227        fireTableRowsUpdated(i - 1, i);
228    }
229
230    public void removeFilter(int i) {
231        filters.remove(i);
232        savePrefs();
233        updateFilters();
234        fireTableRowsDeleted(i, i);
235    }
236
237    public void setFilter(int i, Filter f) {
238        filters.set(i, f);
239        savePrefs();
240        updateFilters();
241        fireTableRowsUpdated(i, i);
242    }
243
244    public Filter getFilter(int i) {
245        return filters.get(i);
246    }
247
248    @Override
249    public int getRowCount() {
250        return filters.size();
251    }
252
253    @Override
254    public int getColumnCount() {
255        return 5;
256    }
257
258    @Override
259    public String getColumnName(int column) {
260        String[] names = {/* translators notes must be in front */
261                /* column header: enable filter */trc("filter", "E"),
262                /* column header: hide filter */trc("filter", "H"),
263                /* column header: filter text */trc("filter", "Text"),
264                /* column header: inverted filter */trc("filter", "I"),
265                /* column header: filter mode */trc("filter", "M")};
266        return names[column];
267    }
268
269    @Override
270    public Class<?> getColumnClass(int column) {
271        Class<?>[] classes = {Boolean.class, Boolean.class, String.class, Boolean.class, String.class};
272        return classes[column];
273    }
274
275    public boolean isCellEnabled(int row, int column) {
276        if (!filters.get(row).enable && column != 0)
277            return false;
278        return true;
279    }
280
281    @Override
282    public boolean isCellEditable(int row, int column) {
283        if (!filters.get(row).enable && column != 0)
284            return false;
285        if (column < 4)
286            return true;
287        return false;
288    }
289
290    @Override
291    public void setValueAt(Object aValue, int row, int column) {
292        if (row >= filters.size()) {
293            return;
294        }
295        Filter f = filters.get(row);
296        switch (column) {
297        case COL_ENABLED:
298            f.enable = (Boolean) aValue;
299            savePrefs();
300            updateFilters();
301            fireTableRowsUpdated(row, row);
302            break;
303        case COL_HIDING:
304            f.hiding = (Boolean) aValue;
305            savePrefs();
306            updateFilters();
307            break;
308        case COL_TEXT:
309            f.text = (String) aValue;
310            savePrefs();
311            break;
312        case COL_INVERTED:
313            f.inverted = (Boolean) aValue;
314            savePrefs();
315            updateFilters();
316            break;
317        default: // Do nothing
318        }
319        if (column != 0) {
320            fireTableCellUpdated(row, column);
321        }
322    }
323
324    @Override
325    public Object getValueAt(int row, int column) {
326        if (row >= filters.size()) {
327            return null;
328        }
329        Filter f = filters.get(row);
330        switch (column) {
331        case COL_ENABLED:
332            return f.enable;
333        case COL_HIDING:
334            return f.hiding;
335        case COL_TEXT:
336            return f.text;
337        case COL_INVERTED:
338            return f.inverted;
339        case 4:
340            switch (f.mode) { /* translators notes must be in front */
341            case replace: /* filter mode: replace */
342                return trc("filter", "R");
343            case add: /* filter mode: add */
344                return trc("filter", "A");
345            case remove: /* filter mode: remove */
346                return trc("filter", "D");
347            case in_selection: /* filter mode: in selection */
348                return trc("filter", "F");
349            default:
350                Main.warn("Unknown filter mode: " + f.mode);
351            }
352            break;
353        default: // Do nothing
354        }
355        return null;
356    }
357
358    /**
359     * On screen display label
360     */
361    private static class OSDLabel extends JLabel {
362        OSDLabel(String text) {
363            super(text);
364            setOpaque(true);
365            setForeground(Color.black);
366            setBackground(new Color(0, 0, 0, 0));
367            setFont(getFont().deriveFont(Font.PLAIN));
368            setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10));
369        }
370
371        @Override
372        public void paintComponent(Graphics g) {
373            g.setColor(new Color(255, 255, 255, 140));
374            g.fillRoundRect(getX(), getY(), getWidth(), getHeight(), 10, 10);
375            super.paintComponent(g);
376        }
377    }
378
379    private final OSDLabel lblOSD = new OSDLabel("");
380
381    public void drawOSDText(Graphics2D g) {
382        String message = "<html>" + tr("<h2>Filter active</h2>");
383
384        if (disabledCount == 0 && disabledAndHiddenCount == 0)
385            return;
386
387        if (disabledAndHiddenCount != 0) {
388            /* for correct i18n of plural forms - see #9110 */
389            message += trn("<p><b>{0}</b> object hidden", "<p><b>{0}</b> objects hidden", disabledAndHiddenCount, disabledAndHiddenCount);
390        }
391
392        if (disabledAndHiddenCount != 0 && disabledCount != 0) {
393            message += "<br>";
394        }
395
396        if (disabledCount != 0) {
397            /* for correct i18n of plural forms - see #9110 */
398            message += trn("<b>{0}</b> object disabled", "<b>{0}</b> objects disabled", disabledCount, disabledCount);
399        }
400
401        message += tr("</p><p>Close the filter dialog to see all objects.<p></html>");
402
403        lblOSD.setText(message);
404        lblOSD.setSize(lblOSD.getPreferredSize());
405
406        int dx = Main.map.mapView.getWidth() - lblOSD.getPreferredSize().width - 15;
407        int dy = 15;
408        g.translate(dx, dy);
409        lblOSD.paintComponent(g);
410        g.translate(-dx, -dy);
411    }
412
413    /**
414     * Returns the list of filters.
415     * @return the list of filters
416     */
417    public List<Filter> getFilters() {
418        return filters;
419    }
420}