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