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    static public final String PROP_DIRTY = TagEditorModel.class.getName() + ".dirty";
036
037    /** the list holding the tags */
038    protected final List<TagModel> tags =new ArrayList<TagModel>();
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</name> yet, a new {@link TagModel} is created
197     * and append to this model.
198     *
199     * If there is a tag with name <code>name</name>, <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        TagModel tagModel = tags.get(idx);
241        return tagModel;
242    }
243
244    @Override 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<TagModel>();
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<String, String>();
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        String newkey = tag.getName();
487        String newvalue = tag.getValue();
488
489        ChangePropertyCommand command = new ChangePropertyCommand(primitives,newkey, newvalue);
490        return command;
491    }
492
493    protected Command createDeleteTagsCommand(Collection<OsmPrimitive> primitives) {
494
495        List<String> currentkeys = getKeys();
496        ArrayList<Command> commands = new ArrayList<Command>();
497
498        for (OsmPrimitive primitive : primitives) {
499            for (String oldkey : primitive.keySet()) {
500                if (!currentkeys.contains(oldkey)) {
501                    ChangePropertyCommand deleteCommand =
502                        new ChangePropertyCommand(primitive,oldkey,null);
503                    commands.add(deleteCommand);
504                }
505            }
506        }
507
508        SequenceCommand command = 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        return command;
514    }
515
516    /**
517     * replies the list of keys of the tags managed by this model
518     *
519     * @return the list of keys managed by this model
520     */
521    public List<String> getKeys() {
522        ArrayList<String> keys = new ArrayList<String>();
523        for (TagModel tag: tags) {
524            if (!tag.getName().trim().isEmpty()) {
525                keys.add(tag.getName());
526            }
527        }
528        return keys;
529    }
530
531    /**
532     * sorts the current tags according alphabetical order of names
533     */
534    protected void sort() {
535        java.util.Collections.sort(
536                tags,
537                new Comparator<TagModel>() {
538                    @Override
539                    public int compare(TagModel self, TagModel other) {
540                        return self.getName().compareTo(other.getName());
541                    }
542                }
543        );
544    }
545
546    /**
547     * updates the name of a tag and sets the dirty state to  true if
548     * the new name is different from the old name.
549     *
550     * @param tag   the tag
551     * @param newName  the new name
552     */
553    public void updateTagName(TagModel tag, String newName) {
554        String oldName = tag.getName();
555        tag.setName(newName);
556        if (! newName.equals(oldName)) {
557            setDirty(true);
558        }
559        SelectionStateMemento memento = new SelectionStateMemento();
560        fireTableDataChanged();
561        memento.apply();
562    }
563
564    /**
565     * updates the value value of a tag and sets the dirty state to true if the
566     * new name is different from the old name
567     *
568     * @param tag  the tag
569     * @param newValue  the new value
570     */
571    public void updateTagValue(TagModel tag, String newValue) {
572        String oldValue = tag.getValue();
573        tag.setValue(newValue);
574        if (! newValue.equals(oldValue)) {
575            setDirty(true);
576        }
577        SelectionStateMemento memento = new SelectionStateMemento();
578        fireTableDataChanged();
579        memento.apply();
580    }
581
582    /**
583     * Load tags from given list
584     * @param tags - the list
585     */
586    public void updateTags(List<Tag> tags) {
587         if (tags.isEmpty())
588            return;
589
590        Map<String, TagModel> modelTags = new HashMap<String, TagModel>();
591        for (int i=0; i<getRowCount(); i++) {
592            TagModel tagModel = get(i);
593            modelTags.put(tagModel.getName(), tagModel);
594        }
595        for (Tag tag: tags) {
596            TagModel existing = modelTags.get(tag.getKey());
597
598            if (tag.getValue().isEmpty()) {
599                if (existing != null) {
600                    delete(tag.getKey());
601                }
602            } else {
603                if (existing != null) {
604                    updateTagValue(existing, tag.getValue());
605                } else {
606                    add(tag.getKey(), tag.getValue());
607                }
608            }
609        }
610    }
611
612    /**
613     * replies true, if this model has been updated
614     *
615     * @return true, if this model has been updated
616     */
617    public boolean isDirty() {
618        return dirty;
619    }
620
621    class SelectionStateMemento {
622        private int rowMin;
623        private int rowMax;
624        private int colMin;
625        private int colMax;
626
627        public SelectionStateMemento() {
628            rowMin = rowSelectionModel.getMinSelectionIndex();
629            rowMax = rowSelectionModel.getMaxSelectionIndex();
630            colMin = colSelectionModel.getMinSelectionIndex();
631            colMax = colSelectionModel.getMaxSelectionIndex();
632        }
633
634        public void apply() {
635            rowSelectionModel.setValueIsAdjusting(true);
636            colSelectionModel.setValueIsAdjusting(true);
637            if (rowMin >= 0 && rowMax >=0) {
638                rowSelectionModel.setSelectionInterval(rowMin, rowMax);
639            }
640            if (colMin >=0 && colMax >= 0) {
641                colSelectionModel.setSelectionInterval(colMin, colMax);
642            }
643            rowSelectionModel.setValueIsAdjusting(false);
644            colSelectionModel.setValueIsAdjusting(false);
645        }
646    }
647}