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        CheckParameterUtil.ensureParameterNotNull(tag, "tag");
179        tags.add(tag);
180        setDirty(true);
181        fireTableDataChanged();
182    }
183
184    public void prepend(TagModel tag) {
185        CheckParameterUtil.ensureParameterNotNull(tag, "tag");
186        tags.add(0, tag);
187        setDirty(true);
188        fireTableDataChanged();
189    }
190
191    /**
192     * adds a tag given by a name/value pair to the tag editor model.
193     *
194     * If there is no tag with name <code>name</code> yet, a new {@link TagModel} is created
195     * and append to this model.
196     *
197     * If there is a tag with name <code>name</code>, <code>value</code> is merged to the list
198     * of values for this tag.
199     *
200     * @param name the name; converted to "" if null
201     * @param value the value; converted to "" if null
202     */
203    public void add(String name, String value) {
204        name = (name == null) ? "" : name;
205        value = (value == null) ? "" : value;
206
207        TagModel tag = get(name);
208        if (tag == null) {
209            tag = new TagModel(name, value);
210            int index = tags.size();
211            while (index >= 1 && tags.get(index - 1).getName().isEmpty() && tags.get(index - 1).getValue().isEmpty()) {
212                index--; // If last line(s) is empty, add new tag before it
213            }
214            tags.add(index, tag);
215        } else {
216            tag.addValue(value);
217        }
218        setDirty(true);
219        fireTableDataChanged();
220    }
221
222    /**
223     * replies the tag with name <code>name</code>; null, if no such tag exists
224     * @param name the tag name
225     * @return the tag with name <code>name</code>; null, if no such tag exists
226     */
227    public TagModel get(String name) {
228        name = (name == null) ? "" : name;
229        for (TagModel tag : tags) {
230            if (tag.getName().equals(name))
231                return tag;
232        }
233        return null;
234    }
235
236    public TagModel get(int idx) {
237        if (idx >= tags.size()) return null;
238        return tags.get(idx);
239    }
240
241    @Override
242    public boolean isCellEditable(int row, int col) {
243        // all cells are editable
244        return true;
245    }
246
247    /**
248     * deletes the names of the tags given by tagIndices
249     *
250     * @param tagIndices a list of tag indices
251     */
252    public void deleteTagNames(int [] tagIndices) {
253        if (tags == null)
254            return;
255        for (int tagIdx : tagIndices) {
256            TagModel tag = tags.get(tagIdx);
257            if (tag != null) {
258                tag.setName("");
259            }
260        }
261        fireTableDataChanged();
262        setDirty(true);
263    }
264
265    /**
266     * deletes the values of the tags given by tagIndices
267     *
268     * @param tagIndices the lit of tag indices
269     */
270    public void deleteTagValues(int [] tagIndices) {
271        if (tags == null)
272            return;
273        for (int tagIdx : tagIndices) {
274            TagModel tag = tags.get(tagIdx);
275            if (tag != null) {
276                tag.setValue("");
277            }
278        }
279        fireTableDataChanged();
280        setDirty(true);
281    }
282
283    /**
284     * Deletes all tags with name <code>name</code>
285     *
286     * @param name the name. Ignored if null.
287     */
288    public void delete(String name) {
289        if (name == null) return;
290        Iterator<TagModel> it = tags.iterator();
291        boolean changed = false;
292        while(it.hasNext()) {
293            TagModel tm = it.next();
294            if (tm.getName().equals(name)) {
295                changed = true;
296                it.remove();
297            }
298        }
299        if (changed) {
300            fireTableDataChanged();
301            setDirty(true);
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        ArrayList<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        setDirty(true);
334    }
335
336    /**
337     * makes sure the model includes at least one (empty) tag
338     */
339    public void ensureOneTag() {
340        if (tags.isEmpty()) {
341            appendNewTag();
342        }
343    }
344
345    /**
346     * initializes the model with the tags of an OSM primitive
347     *
348     * @param primitive the OSM primitive
349     */
350    public void initFromPrimitive(Tagged primitive) {
351        this.tags.clear();
352        for (String key : primitive.keySet()) {
353            String value = primitive.get(key);
354            this.tags.add(new TagModel(key,value));
355        }
356        TagModel tag = new TagModel();
357        sort();
358        tags.add(tag);
359        setDirty(false);
360        fireTableDataChanged();
361    }
362
363    /**
364     * Initializes the model with the tags of an OSM primitive
365     *
366     * @param tags the tags of an OSM primitive
367     */
368    public void initFromTags(Map<String,String> tags) {
369        this.tags.clear();
370        for (Entry<String, String> entry : tags.entrySet()) {
371            this.tags.add(new TagModel(entry.getKey(), entry.getValue()));
372        }
373        sort();
374        TagModel tag = new TagModel();
375        this.tags.add(tag);
376        setDirty(false);
377    }
378
379    /**
380     * Initializes the model with the tags in a tag collection. Removes
381     * all tags if {@code tags} is null.
382     *
383     * @param tags the tags
384     */
385    public void initFromTags(TagCollection tags) {
386        this.tags.clear();
387        if (tags == null){
388            setDirty(false);
389            return;
390        }
391        for (String key : tags.getKeys()) {
392            String value = tags.getJoinedValues(key);
393            this.tags.add(new TagModel(key,value));
394        }
395        sort();
396        // add an empty row
397        TagModel tag = new TagModel();
398        this.tags.add(tag);
399        setDirty(false);
400    }
401
402    /**
403     * applies the current state of the tag editor model to a primitive
404     *
405     * @param primitive the primitive
406     *
407     */
408    public void applyToPrimitive(Tagged primitive) {
409        Map<String,String> tags = primitive.getKeys();
410        applyToTags(tags, false);
411        primitive.setKeys(tags);
412    }
413
414    /**
415     * applies the current state of the tag editor model to a map of tags
416     *
417     * @param tags the map of key/value pairs
418     *
419     */
420    public void applyToTags(Map<String, String> tags, boolean keepEmpty) {
421        tags.clear();
422        for (TagModel tag: this.tags) {
423            // tag still holds an unchanged list of different values for the same key.
424            // no property change command required
425            if (tag.getValueCount() > 1) {
426                continue;
427            }
428
429            // tag name holds an empty key. Don't apply it to the selection.
430            //
431            if (!keepEmpty && (tag.getName().trim().isEmpty() || tag.getValue().trim().isEmpty())) {
432                continue;
433            }
434            tags.put(tag.getName().trim(), tag.getValue().trim());
435        }
436    }
437
438    public Map<String,String> getTags() {
439        return getTags(false);
440    }
441
442    public Map<String,String> getTags(boolean keepEmpty) {
443        Map<String,String> tags = new HashMap<>();
444        applyToTags(tags, keepEmpty);
445        return tags;
446    }
447
448    /**
449     * Replies the tags in this tag editor model as {@link TagCollection}.
450     *
451     * @return the tags in this tag editor model as {@link TagCollection}
452     */
453    public TagCollection getTagCollection() {
454        return TagCollection.from(getTags());
455    }
456
457    /**
458     * checks whether the tag model includes a tag with a given key
459     *
460     * @param key  the key
461     * @return true, if the tag model includes the tag; false, otherwise
462     */
463    public boolean includesTag(String key) {
464        if (key == null) return false;
465        for (TagModel tag : tags) {
466            if (tag.getName().equals(key))
467                return true;
468        }
469        return false;
470    }
471
472    protected Command createUpdateTagCommand(Collection<OsmPrimitive> primitives, TagModel tag) {
473
474        // tag still holds an unchanged list of different values for the same key.
475        // no property change command required
476        if (tag.getValueCount() > 1)
477            return null;
478
479        // tag name holds an empty key. Don't apply it to the selection.
480        //
481        if (tag.getName().trim().isEmpty())
482            return null;
483
484        return new ChangePropertyCommand(primitives, tag.getName(), tag.getValue());
485    }
486
487    protected Command createDeleteTagsCommand(Collection<OsmPrimitive> primitives) {
488
489        List<String> currentkeys = getKeys();
490        ArrayList<Command> commands = new ArrayList<>();
491
492        for (OsmPrimitive primitive : primitives) {
493            for (String oldkey : primitive.keySet()) {
494                if (!currentkeys.contains(oldkey)) {
495                    ChangePropertyCommand deleteCommand =
496                        new ChangePropertyCommand(primitive,oldkey,null);
497                    commands.add(deleteCommand);
498                }
499            }
500        }
501
502        return new SequenceCommand(
503                trn("Remove old keys from up to {0} object", "Remove old keys from up to {0} objects", primitives.size(), primitives.size()),
504                commands
505        );
506    }
507
508    /**
509     * replies the list of keys of the tags managed by this model
510     *
511     * @return the list of keys managed by this model
512     */
513    public List<String> getKeys() {
514        ArrayList<String> keys = new ArrayList<>();
515        for (TagModel tag: tags) {
516            if (!tag.getName().trim().isEmpty()) {
517                keys.add(tag.getName());
518            }
519        }
520        return keys;
521    }
522
523    /**
524     * sorts the current tags according alphabetical order of names
525     */
526    protected void sort() {
527        java.util.Collections.sort(
528                tags,
529                new Comparator<TagModel>() {
530                    @Override
531                    public int compare(TagModel self, TagModel other) {
532                        return self.getName().compareTo(other.getName());
533                    }
534                }
535        );
536    }
537
538    /**
539     * updates the name of a tag and sets the dirty state to  true if
540     * the new name is different from the old name.
541     *
542     * @param tag   the tag
543     * @param newName  the new name
544     */
545    public void updateTagName(TagModel tag, String newName) {
546        String oldName = tag.getName();
547        tag.setName(newName);
548        if (! newName.equals(oldName)) {
549            setDirty(true);
550        }
551        SelectionStateMemento memento = new SelectionStateMemento();
552        fireTableDataChanged();
553        memento.apply();
554    }
555
556    /**
557     * updates the value value of a tag and sets the dirty state to true if the
558     * new name is different from the old name
559     *
560     * @param tag  the tag
561     * @param newValue  the new value
562     */
563    public void updateTagValue(TagModel tag, String newValue) {
564        String oldValue = tag.getValue();
565        tag.setValue(newValue);
566        if (! newValue.equals(oldValue)) {
567            setDirty(true);
568        }
569        SelectionStateMemento memento = new SelectionStateMemento();
570        fireTableDataChanged();
571        memento.apply();
572    }
573
574    /**
575     * Load tags from given list
576     * @param tags - the list
577     */
578    public void updateTags(List<Tag> tags) {
579         if (tags.isEmpty())
580            return;
581
582        Map<String, TagModel> modelTags = new HashMap<>();
583        for (int i=0; i<getRowCount(); i++) {
584            TagModel tagModel = get(i);
585            modelTags.put(tagModel.getName(), tagModel);
586        }
587        for (Tag tag: tags) {
588            TagModel existing = modelTags.get(tag.getKey());
589
590            if (tag.getValue().isEmpty()) {
591                if (existing != null) {
592                    delete(tag.getKey());
593                }
594            } else {
595                if (existing != null) {
596                    updateTagValue(existing, tag.getValue());
597                } else {
598                    add(tag.getKey(), tag.getValue());
599                }
600            }
601        }
602    }
603
604    /**
605     * replies true, if this model has been updated
606     *
607     * @return true, if this model has been updated
608     */
609    public boolean isDirty() {
610        return dirty;
611    }
612
613    class SelectionStateMemento {
614        private int rowMin;
615        private int rowMax;
616        private int colMin;
617        private int colMax;
618
619        public SelectionStateMemento() {
620            rowMin = rowSelectionModel.getMinSelectionIndex();
621            rowMax = rowSelectionModel.getMaxSelectionIndex();
622            colMin = colSelectionModel.getMinSelectionIndex();
623            colMax = colSelectionModel.getMaxSelectionIndex();
624        }
625
626        public void apply() {
627            rowSelectionModel.setValueIsAdjusting(true);
628            colSelectionModel.setValueIsAdjusting(true);
629            if (rowMin >= 0 && rowMax >=0) {
630                rowSelectionModel.setSelectionInterval(rowMin, rowMax);
631            }
632            if (colMin >=0 && colMax >= 0) {
633                colSelectionModel.setSelectionInterval(colMin, colMax);
634            }
635            rowSelectionModel.setValueIsAdjusting(false);
636            colSelectionModel.setValueIsAdjusting(false);
637        }
638    }
639}