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