001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.tagging;
003
004import static org.openstreetmap.josm.tools.I18n.trn;
005
006import java.beans.PropertyChangeListener;
007import java.beans.PropertyChangeSupport;
008import java.util.ArrayList;
009import java.util.Collection;
010import java.util.Comparator;
011import java.util.EnumSet;
012import java.util.HashMap;
013import java.util.Iterator;
014import java.util.List;
015import java.util.Map;
016import java.util.Map.Entry;
017
018import javax.swing.DefaultListSelectionModel;
019import javax.swing.table.AbstractTableModel;
020
021import org.openstreetmap.josm.command.ChangePropertyCommand;
022import org.openstreetmap.josm.command.Command;
023import org.openstreetmap.josm.command.SequenceCommand;
024import org.openstreetmap.josm.data.osm.OsmPrimitive;
025import org.openstreetmap.josm.data.osm.Tag;
026import org.openstreetmap.josm.data.osm.TagCollection;
027import org.openstreetmap.josm.data.osm.Tagged;
028import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
029import org.openstreetmap.josm.tools.CheckParameterUtil;
030
031/**
032 * TagEditorModel is a table model to use with {@link TagEditorPanel}.
033 * @since 1762
034 */
035public class TagEditorModel extends AbstractTableModel {
036    public static final String PROP_DIRTY = TagEditorModel.class.getName() + ".dirty";
037
038    /** the list holding the tags */
039    protected final transient List<TagModel> tags = new ArrayList<>();
040
041    /** indicates whether the model is dirty */
042    private boolean dirty;
043    private final PropertyChangeSupport propChangeSupport = new PropertyChangeSupport(this);
044
045    private final DefaultListSelectionModel rowSelectionModel;
046    private final DefaultListSelectionModel colSelectionModel;
047
048    private transient OsmPrimitive primitive;
049
050    /**
051     * Creates a new tag editor model. Internally allocates two selection models
052     * for row selection and column selection.
053     *
054     * To create a {@link javax.swing.JTable} with this model:
055     * <pre>
056     *    TagEditorModel model = new TagEditorModel();
057     *    TagTable tbl  = new TagTabel(model);
058     * </pre>
059     *
060     * @see #getRowSelectionModel()
061     * @see #getColumnSelectionModel()
062     */
063    public TagEditorModel() {
064        this(new DefaultListSelectionModel(), new DefaultListSelectionModel());
065    }
066
067    /**
068     * Creates a new tag editor model.
069     *
070     * @param rowSelectionModel the row selection model. Must not be null.
071     * @param colSelectionModel the column selection model. Must not be null.
072     * @throws IllegalArgumentException if {@code rowSelectionModel} is null
073     * @throws IllegalArgumentException if {@code colSelectionModel} is null
074     */
075    public TagEditorModel(DefaultListSelectionModel rowSelectionModel, DefaultListSelectionModel colSelectionModel) {
076        CheckParameterUtil.ensureParameterNotNull(rowSelectionModel, "rowSelectionModel");
077        CheckParameterUtil.ensureParameterNotNull(colSelectionModel, "colSelectionModel");
078        this.rowSelectionModel = rowSelectionModel;
079        this.colSelectionModel = colSelectionModel;
080    }
081
082    /**
083     * Adds property change listener.
084     * @param listener property change listener to add
085     */
086    public void addPropertyChangeListener(PropertyChangeListener listener) {
087        propChangeSupport.addPropertyChangeListener(listener);
088    }
089
090    /**
091     * Replies the row selection model used by this tag editor model
092     *
093     * @return the row selection model used by this tag editor model
094     */
095    public DefaultListSelectionModel getRowSelectionModel() {
096        return rowSelectionModel;
097    }
098
099    /**
100     * Replies the column selection model used by this tag editor model
101     *
102     * @return the column selection model used by this tag editor model
103     */
104    public DefaultListSelectionModel getColumnSelectionModel() {
105        return colSelectionModel;
106    }
107
108    /**
109     * Removes property change listener.
110     * @param listener property change listener to remove
111     */
112    public void removePropertyChangeListener(PropertyChangeListener listener) {
113        propChangeSupport.removePropertyChangeListener(listener);
114    }
115
116    protected void fireDirtyStateChanged(final boolean oldValue, final boolean newValue) {
117        propChangeSupport.firePropertyChange(PROP_DIRTY, oldValue, newValue);
118    }
119
120    protected void setDirty(boolean newValue) {
121        boolean oldValue = dirty;
122        dirty = newValue;
123        if (oldValue != newValue) {
124            fireDirtyStateChanged(oldValue, newValue);
125        }
126    }
127
128    @Override
129    public int getColumnCount() {
130        return 2;
131    }
132
133    @Override
134    public int getRowCount() {
135        return tags.size();
136    }
137
138    @Override
139    public Object getValueAt(int rowIndex, int columnIndex) {
140        if (rowIndex >= getRowCount())
141            throw new IndexOutOfBoundsException("unexpected rowIndex: rowIndex=" + rowIndex);
142
143        return tags.get(rowIndex);
144    }
145
146    @Override
147    public void setValueAt(Object value, int row, int col) {
148        TagModel tag = get(row);
149        if (tag != null) {
150            switch(col) {
151            case 0:
152                updateTagName(tag, (String) value);
153                break;
154            case 1:
155                String v = (String) value;
156                if ((tag.getValueCount() > 1 && !v.isEmpty()) || tag.getValueCount() <= 1) {
157                    updateTagValue(tag, v);
158                }
159                break;
160            default: // Do nothing
161            }
162        }
163    }
164
165    /**
166     * removes all tags in the model
167     */
168    public void clear() {
169        boolean wasEmpty = tags.isEmpty();
170        tags.clear();
171        if (!wasEmpty) {
172            setDirty(true);
173            fireTableDataChanged();
174        }
175    }
176
177    /**
178     * adds a tag to the model
179     *
180     * @param tag the tag. Must not be null.
181     *
182     * @throws IllegalArgumentException if tag is null
183     */
184    public void add(TagModel tag) {
185        CheckParameterUtil.ensureParameterNotNull(tag, "tag");
186        tags.add(tag);
187        setDirty(true);
188        fireTableDataChanged();
189    }
190
191    public void prepend(TagModel tag) {
192        CheckParameterUtil.ensureParameterNotNull(tag, "tag");
193        tags.add(0, tag);
194        setDirty(true);
195        fireTableDataChanged();
196    }
197
198    /**
199     * adds a tag given by a name/value pair to the tag editor model.
200     *
201     * If there is no tag with name <code>name</code> yet, a new {@link TagModel} is created
202     * and append to this model.
203     *
204     * If there is a tag with name <code>name</code>, <code>value</code> is merged to the list
205     * of values for this tag.
206     *
207     * @param name the name; converted to "" if null
208     * @param value the value; converted to "" if null
209     */
210    public void add(String name, String value) {
211        String key = (name == null) ? "" : name;
212        String val = (value == null) ? "" : value;
213
214        TagModel tag = get(key);
215        if (tag == null) {
216            tag = new TagModel(key, val);
217            int index = tags.size();
218            while (index >= 1 && tags.get(index - 1).getName().isEmpty() && tags.get(index - 1).getValue().isEmpty()) {
219                index--; // If last line(s) is empty, add new tag before it
220            }
221            tags.add(index, tag);
222        } else {
223            tag.addValue(val);
224        }
225        setDirty(true);
226        fireTableDataChanged();
227    }
228
229    /**
230     * replies the tag with name <code>name</code>; null, if no such tag exists
231     * @param name the tag name
232     * @return the tag with name <code>name</code>; null, if no such tag exists
233     */
234    public TagModel get(String name) {
235        String key = (name == null) ? "" : name;
236        for (TagModel tag : tags) {
237            if (tag.getName().equals(key))
238                return tag;
239        }
240        return null;
241    }
242
243    public TagModel get(int idx) {
244        return idx >= tags.size() ? null : tags.get(idx);
245    }
246
247    @Override
248    public boolean isCellEditable(int row, int col) {
249        // all cells are editable
250        return true;
251    }
252
253    /**
254     * deletes the names of the tags given by tagIndices
255     *
256     * @param tagIndices a list of tag indices
257     */
258    public void deleteTagNames(int[] tagIndices) {
259        if (tags == null)
260            return;
261        for (int tagIdx : tagIndices) {
262            TagModel tag = tags.get(tagIdx);
263            if (tag != null) {
264                tag.setName("");
265            }
266        }
267        fireTableDataChanged();
268        setDirty(true);
269    }
270
271    /**
272     * deletes the values of the tags given by tagIndices
273     *
274     * @param tagIndices the lit of tag indices
275     */
276    public void deleteTagValues(int[] tagIndices) {
277        if (tags == null)
278            return;
279        for (int tagIdx : tagIndices) {
280            TagModel tag = tags.get(tagIdx);
281            if (tag != null) {
282                tag.setValue("");
283            }
284        }
285        fireTableDataChanged();
286        setDirty(true);
287    }
288
289    /**
290     * Deletes all tags with name <code>name</code>
291     *
292     * @param name the name. Ignored if null.
293     */
294    public void delete(String name) {
295        if (name == null)
296            return;
297        Iterator<TagModel> it = tags.iterator();
298        boolean changed = false;
299        while (it.hasNext()) {
300            TagModel tm = it.next();
301            if (tm.getName().equals(name)) {
302                changed = true;
303                it.remove();
304            }
305        }
306        if (changed) {
307            fireTableDataChanged();
308            setDirty(true);
309        }
310    }
311
312    /**
313     * deletes the tags given by tagIndices
314     *
315     * @param tagIndices the list of tag indices
316     */
317    public void deleteTags(int[] tagIndices) {
318        if (tags == null)
319            return;
320        List<TagModel> toDelete = new ArrayList<>();
321        for (int tagIdx : tagIndices) {
322            TagModel tag = tags.get(tagIdx);
323            if (tag != null) {
324                toDelete.add(tag);
325            }
326        }
327        for (TagModel tag : toDelete) {
328            tags.remove(tag);
329        }
330        fireTableDataChanged();
331        setDirty(true);
332    }
333
334    /**
335     * creates a new tag and appends it to the model
336     */
337    public void appendNewTag() {
338        TagModel tag = new TagModel();
339        tags.add(tag);
340        fireTableDataChanged();
341    }
342
343    /**
344     * makes sure the model includes at least one (empty) tag
345     */
346    public void ensureOneTag() {
347        if (tags.isEmpty()) {
348            appendNewTag();
349        }
350    }
351
352    /**
353     * initializes the model with the tags of an OSM primitive
354     *
355     * @param primitive the OSM primitive
356     */
357    public void initFromPrimitive(Tagged primitive) {
358        this.tags.clear();
359        for (String key : primitive.keySet()) {
360            String value = primitive.get(key);
361            this.tags.add(new TagModel(key, value));
362        }
363        TagModel tag = new TagModel();
364        sort();
365        tags.add(tag);
366        setDirty(false);
367        fireTableDataChanged();
368    }
369
370    /**
371     * Initializes the model with the tags of an OSM primitive
372     *
373     * @param tags the tags of an OSM primitive
374     */
375    public void initFromTags(Map<String, String> tags) {
376        this.tags.clear();
377        for (Entry<String, String> entry : tags.entrySet()) {
378            this.tags.add(new TagModel(entry.getKey(), entry.getValue()));
379        }
380        sort();
381        TagModel tag = new TagModel();
382        this.tags.add(tag);
383        setDirty(false);
384    }
385
386    /**
387     * Initializes the model with the tags in a tag collection. Removes
388     * all tags if {@code tags} is null.
389     *
390     * @param tags the tags
391     */
392    public void initFromTags(TagCollection tags) {
393        this.tags.clear();
394        if (tags == null) {
395            setDirty(false);
396            return;
397        }
398        for (String key : tags.getKeys()) {
399            String value = tags.getJoinedValues(key);
400            this.tags.add(new TagModel(key, value));
401        }
402        sort();
403        // add an empty row
404        TagModel tag = new TagModel();
405        this.tags.add(tag);
406        setDirty(false);
407    }
408
409    /**
410     * applies the current state of the tag editor model to a primitive
411     *
412     * @param primitive the primitive
413     *
414     */
415    public void applyToPrimitive(Tagged primitive) {
416        primitive.setKeys(applyToTags(false));
417    }
418
419    /**
420     * applies the current state of the tag editor model to a map of tags
421     * @param keepEmpty {@code true} to keep empty tags
422     *
423     * @return the map of key/value pairs
424     */
425    private Map<String, String> applyToTags(boolean keepEmpty) {
426        Map<String, String> result = new HashMap<>();
427        for (TagModel tag: this.tags) {
428            // tag still holds an unchanged list of different values for the same key.
429            // no property change command required
430            if (tag.getValueCount() > 1) {
431                continue;
432            }
433
434            // tag name holds an empty key. Don't apply it to the selection.
435            //
436            if (!keepEmpty && (tag.getName().trim().isEmpty() || tag.getValue().trim().isEmpty())) {
437                continue;
438            }
439            result.put(tag.getName().trim(), tag.getValue().trim());
440        }
441        return result;
442    }
443
444    /**
445     * Returns tags, without empty ones.
446     * @return not-empty tags
447     */
448    public Map<String, String> getTags() {
449        return getTags(false);
450    }
451
452    /**
453     * Returns tags.
454     * @param keepEmpty {@code true} to keep empty tags
455     * @return tags
456     */
457    public Map<String, String> getTags(boolean keepEmpty) {
458        return applyToTags(keepEmpty);
459    }
460
461    /**
462     * Replies the tags in this tag editor model as {@link TagCollection}.
463     *
464     * @return the tags in this tag editor model as {@link TagCollection}
465     */
466    public TagCollection getTagCollection() {
467        return TagCollection.from(getTags());
468    }
469
470    /**
471     * checks whether the tag model includes a tag with a given key
472     *
473     * @param key  the key
474     * @return true, if the tag model includes the tag; false, otherwise
475     */
476    public boolean includesTag(String key) {
477        if (key != null) {
478            for (TagModel tag : tags) {
479                if (tag.getName().equals(key))
480                    return true;
481            }
482        }
483        return false;
484    }
485
486    protected Command createUpdateTagCommand(Collection<OsmPrimitive> primitives, TagModel tag) {
487
488        // tag still holds an unchanged list of different values for the same key.
489        // no property change command required
490        if (tag.getValueCount() > 1)
491            return null;
492
493        // tag name holds an empty key. Don't apply it to the selection.
494        //
495        if (tag.getName().trim().isEmpty())
496            return null;
497
498        return new ChangePropertyCommand(primitives, tag.getName(), tag.getValue());
499    }
500
501    protected Command createDeleteTagsCommand(Collection<OsmPrimitive> primitives) {
502
503        List<String> currentkeys = getKeys();
504        List<Command> commands = new ArrayList<>();
505
506        for (OsmPrimitive prim : primitives) {
507            for (String oldkey : prim.keySet()) {
508                if (!currentkeys.contains(oldkey)) {
509                    ChangePropertyCommand deleteCommand =
510                        new ChangePropertyCommand(prim, oldkey, null);
511                    commands.add(deleteCommand);
512                }
513            }
514        }
515
516        return new SequenceCommand(
517                trn("Remove old keys from up to {0} object", "Remove old keys from up to {0} objects", primitives.size(), primitives.size()),
518                commands
519        );
520    }
521
522    /**
523     * replies the list of keys of the tags managed by this model
524     *
525     * @return the list of keys managed by this model
526     */
527    public List<String> getKeys() {
528        List<String> keys = new ArrayList<>();
529        for (TagModel tag: tags) {
530            if (!tag.getName().trim().isEmpty()) {
531                keys.add(tag.getName());
532            }
533        }
534        return keys;
535    }
536
537    /**
538     * sorts the current tags according alphabetical order of names
539     */
540    protected void sort() {
541        java.util.Collections.sort(
542                tags,
543                new Comparator<TagModel>() {
544                    @Override
545                    public int compare(TagModel self, TagModel other) {
546                        return self.getName().compareTo(other.getName());
547                    }
548                }
549        );
550    }
551
552    /**
553     * updates the name of a tag and sets the dirty state to  true if
554     * the new name is different from the old name.
555     *
556     * @param tag   the tag
557     * @param newName  the new name
558     */
559    public void updateTagName(TagModel tag, String newName) {
560        String oldName = tag.getName();
561        tag.setName(newName);
562        if (!newName.equals(oldName)) {
563            setDirty(true);
564        }
565        SelectionStateMemento memento = new SelectionStateMemento();
566        fireTableDataChanged();
567        memento.apply();
568    }
569
570    /**
571     * updates the value value of a tag and sets the dirty state to true if the
572     * new name is different from the old name
573     *
574     * @param tag  the tag
575     * @param newValue  the new value
576     */
577    public void updateTagValue(TagModel tag, String newValue) {
578        String oldValue = tag.getValue();
579        tag.setValue(newValue);
580        if (!newValue.equals(oldValue)) {
581            setDirty(true);
582        }
583        SelectionStateMemento memento = new SelectionStateMemento();
584        fireTableDataChanged();
585        memento.apply();
586    }
587
588    /**
589     * Load tags from given list
590     * @param tags - the list
591     */
592    public void updateTags(List<Tag> tags) {
593         if (tags.isEmpty())
594            return;
595
596        Map<String, TagModel> modelTags = new HashMap<>();
597        for (int i = 0; i < getRowCount(); i++) {
598            TagModel tagModel = get(i);
599            modelTags.put(tagModel.getName(), tagModel);
600        }
601        for (Tag tag: tags) {
602            TagModel existing = modelTags.get(tag.getKey());
603
604            if (tag.getValue().isEmpty()) {
605                if (existing != null) {
606                    delete(tag.getKey());
607                }
608            } else {
609                if (existing != null) {
610                    updateTagValue(existing, tag.getValue());
611                } else {
612                    add(tag.getKey(), tag.getValue());
613                }
614            }
615        }
616    }
617
618    /**
619     * replies true, if this model has been updated
620     *
621     * @return true, if this model has been updated
622     */
623    public boolean isDirty() {
624        return dirty;
625    }
626
627    /**
628     * Returns the list of tagging presets types to consider when updating the presets list panel.
629     * By default returns type of associated primitive or empty set.
630     * @return the list of tagging presets types to consider when updating the presets list panel
631     * @see #forPrimitive
632     * @see TaggingPresetType#forPrimitive
633     * @since 9588
634     */
635    public Collection<TaggingPresetType> getTaggingPresetTypes() {
636        return primitive == null ? EnumSet.noneOf(TaggingPresetType.class) : EnumSet.of(TaggingPresetType.forPrimitive(primitive));
637    }
638
639    /**
640     * Makes this TagEditorModel specific to a given OSM primitive.
641     * @param primitive primitive to consider
642     * @return {@code this}
643     * @since 9588
644     */
645    public TagEditorModel forPrimitive(OsmPrimitive primitive) {
646        this.primitive = primitive;
647        return this;
648    }
649
650    class SelectionStateMemento {
651        private final int rowMin;
652        private final int rowMax;
653        private final int colMin;
654        private final int colMax;
655
656        SelectionStateMemento() {
657            rowMin = rowSelectionModel.getMinSelectionIndex();
658            rowMax = rowSelectionModel.getMaxSelectionIndex();
659            colMin = colSelectionModel.getMinSelectionIndex();
660            colMax = colSelectionModel.getMaxSelectionIndex();
661        }
662
663        void apply() {
664            rowSelectionModel.setValueIsAdjusting(true);
665            colSelectionModel.setValueIsAdjusting(true);
666            if (rowMin >= 0 && rowMax >= 0) {
667                rowSelectionModel.setSelectionInterval(rowMin, rowMax);
668            }
669            if (colMin >= 0 && colMax >= 0) {
670                colSelectionModel.setSelectionInterval(colMin, colMax);
671            }
672            rowSelectionModel.setValueIsAdjusting(false);
673            colSelectionModel.setValueIsAdjusting(false);
674        }
675    }
676}