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 */
033public class TagEditorModel extends AbstractTableModel {
034    public static final String PROP_DIRTY = TagEditorModel.class.getName() + ".dirty";
035
036    /** the list holding the tags */
037    protected final transient List<TagModel> tags = new ArrayList<>();
038
039    /** indicates whether the model is dirty */
040    private boolean dirty;
041    private final PropertyChangeSupport propChangeSupport = new PropertyChangeSupport(this);
042
043    private DefaultListSelectionModel rowSelectionModel;
044    private DefaultListSelectionModel colSelectionModel;
045
046    /**
047     * Creates a new tag editor model. Internally allocates two selection models
048     * for row selection and column selection.
049     *
050     * To create a {@link javax.swing.JTable} with this model:
051     * <pre>
052     *    TagEditorModel model = new TagEditorModel();
053     *    TagTable tbl  = new TagTabel(model);
054     * </pre>
055     *
056     * @see #getRowSelectionModel()
057     * @see #getColumnSelectionModel()
058     */
059    public TagEditorModel() {
060        this.rowSelectionModel = new DefaultListSelectionModel();
061        this.colSelectionModel  = new DefaultListSelectionModel();
062    }
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 if {@code rowSelectionModel} is null
070     * @throws IllegalArgumentException if {@code colSelectionModel} is null
071     */
072    public TagEditorModel(DefaultListSelectionModel rowSelectionModel, DefaultListSelectionModel colSelectionModel) {
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     * @throws IllegalArgumentException 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    /**
306     * deletes the tags given by tagIndices
307     *
308     * @param tagIndices the list of tag indices
309     */
310    public void deleteTags(int[] tagIndices) {
311        if (tags == null)
312            return;
313        List<TagModel> toDelete = new ArrayList<>();
314        for (int tagIdx : tagIndices) {
315            TagModel tag = tags.get(tagIdx);
316            if (tag != null) {
317                toDelete.add(tag);
318            }
319        }
320        for (TagModel tag : toDelete) {
321            tags.remove(tag);
322        }
323        fireTableDataChanged();
324        setDirty(true);
325    }
326
327    /**
328     * creates a new tag and appends it to the model
329     */
330    public void appendNewTag() {
331        TagModel tag = new TagModel();
332        tags.add(tag);
333        fireTableDataChanged();
334        setDirty(true);
335    }
336
337    /**
338     * makes sure the model includes at least one (empty) tag
339     */
340    public void ensureOneTag() {
341        if (tags.isEmpty()) {
342            appendNewTag();
343        }
344    }
345
346    /**
347     * initializes the model with the tags of an OSM primitive
348     *
349     * @param primitive the OSM primitive
350     */
351    public void initFromPrimitive(Tagged primitive) {
352        this.tags.clear();
353        for (String key : primitive.keySet()) {
354            String value = primitive.get(key);
355            this.tags.add(new TagModel(key, value));
356        }
357        TagModel tag = new TagModel();
358        sort();
359        tags.add(tag);
360        setDirty(false);
361        fireTableDataChanged();
362    }
363
364    /**
365     * Initializes the model with the tags of an OSM primitive
366     *
367     * @param tags the tags of an OSM primitive
368     */
369    public void initFromTags(Map<String, String> tags) {
370        this.tags.clear();
371        for (Entry<String, String> entry : tags.entrySet()) {
372            this.tags.add(new TagModel(entry.getKey(), entry.getValue()));
373        }
374        sort();
375        TagModel tag = new TagModel();
376        this.tags.add(tag);
377        setDirty(false);
378    }
379
380    /**
381     * Initializes the model with the tags in a tag collection. Removes
382     * all tags if {@code tags} is null.
383     *
384     * @param tags the tags
385     */
386    public void initFromTags(TagCollection tags) {
387        this.tags.clear();
388        if (tags == null) {
389            setDirty(false);
390            return;
391        }
392        for (String key : tags.getKeys()) {
393            String value = tags.getJoinedValues(key);
394            this.tags.add(new TagModel(key, value));
395        }
396        sort();
397        // add an empty row
398        TagModel tag = new TagModel();
399        this.tags.add(tag);
400        setDirty(false);
401    }
402
403    /**
404     * applies the current state of the tag editor model to a primitive
405     *
406     * @param primitive the primitive
407     *
408     */
409    public void applyToPrimitive(Tagged primitive) {
410        primitive.setKeys(applyToTags(false));
411    }
412
413    /**
414     * applies the current state of the tag editor model to a map of tags
415     *
416     * @return the map of key/value pairs
417     */
418    private Map<String, String> applyToTags(boolean keepEmpty) {
419        Map<String, String> result = new HashMap<>();
420        for (TagModel tag: this.tags) {
421            // tag still holds an unchanged list of different values for the same key.
422            // no property change command required
423            if (tag.getValueCount() > 1) {
424                continue;
425            }
426
427            // tag name holds an empty key. Don't apply it to the selection.
428            //
429            if (!keepEmpty && (tag.getName().trim().isEmpty() || tag.getValue().trim().isEmpty())) {
430                continue;
431            }
432            result.put(tag.getName().trim(), tag.getValue().trim());
433        }
434        return result;
435    }
436
437    public Map<String, String> getTags() {
438        return getTags(false);
439    }
440
441    public Map<String, String> getTags(boolean keepEmpty) {
442        return applyToTags(keepEmpty);
443    }
444
445    /**
446     * Replies the tags in this tag editor model as {@link TagCollection}.
447     *
448     * @return the tags in this tag editor model as {@link TagCollection}
449     */
450    public TagCollection getTagCollection() {
451        return TagCollection.from(getTags());
452    }
453
454    /**
455     * checks whether the tag model includes a tag with a given key
456     *
457     * @param key  the key
458     * @return true, if the tag model includes the tag; false, otherwise
459     */
460    public boolean includesTag(String key) {
461        if (key == null) return false;
462        for (TagModel tag : tags) {
463            if (tag.getName().equals(key))
464                return true;
465        }
466        return false;
467    }
468
469    protected Command createUpdateTagCommand(Collection<OsmPrimitive> primitives, TagModel tag) {
470
471        // tag still holds an unchanged list of different values for the same key.
472        // no property change command required
473        if (tag.getValueCount() > 1)
474            return null;
475
476        // tag name holds an empty key. Don't apply it to the selection.
477        //
478        if (tag.getName().trim().isEmpty())
479            return null;
480
481        return new ChangePropertyCommand(primitives, tag.getName(), tag.getValue());
482    }
483
484    protected Command createDeleteTagsCommand(Collection<OsmPrimitive> primitives) {
485
486        List<String> currentkeys = getKeys();
487        List<Command> commands = new ArrayList<>();
488
489        for (OsmPrimitive primitive : primitives) {
490            for (String oldkey : primitive.keySet()) {
491                if (!currentkeys.contains(oldkey)) {
492                    ChangePropertyCommand deleteCommand =
493                        new ChangePropertyCommand(primitive, oldkey, null);
494                    commands.add(deleteCommand);
495                }
496            }
497        }
498
499        return new SequenceCommand(
500                trn("Remove old keys from up to {0} object", "Remove old keys from up to {0} objects", primitives.size(), primitives.size()),
501                commands
502        );
503    }
504
505    /**
506     * replies the list of keys of the tags managed by this model
507     *
508     * @return the list of keys managed by this model
509     */
510    public List<String> getKeys() {
511        List<String> keys = new ArrayList<>();
512        for (TagModel tag: tags) {
513            if (!tag.getName().trim().isEmpty()) {
514                keys.add(tag.getName());
515            }
516        }
517        return keys;
518    }
519
520    /**
521     * sorts the current tags according alphabetical order of names
522     */
523    protected void sort() {
524        java.util.Collections.sort(
525                tags,
526                new Comparator<TagModel>() {
527                    @Override
528                    public int compare(TagModel self, TagModel other) {
529                        return self.getName().compareTo(other.getName());
530                    }
531                }
532        );
533    }
534
535    /**
536     * updates the name of a tag and sets the dirty state to  true if
537     * the new name is different from the old name.
538     *
539     * @param tag   the tag
540     * @param newName  the new name
541     */
542    public void updateTagName(TagModel tag, String newName) {
543        String oldName = tag.getName();
544        tag.setName(newName);
545        if (!newName.equals(oldName)) {
546            setDirty(true);
547        }
548        SelectionStateMemento memento = new SelectionStateMemento();
549        fireTableDataChanged();
550        memento.apply();
551    }
552
553    /**
554     * updates the value value of a tag and sets the dirty state to true if the
555     * new name is different from the old name
556     *
557     * @param tag  the tag
558     * @param newValue  the new value
559     */
560    public void updateTagValue(TagModel tag, String newValue) {
561        String oldValue = tag.getValue();
562        tag.setValue(newValue);
563        if (!newValue.equals(oldValue)) {
564            setDirty(true);
565        }
566        SelectionStateMemento memento = new SelectionStateMemento();
567        fireTableDataChanged();
568        memento.apply();
569    }
570
571    /**
572     * Load tags from given list
573     * @param tags - the list
574     */
575    public void updateTags(List<Tag> tags) {
576         if (tags.isEmpty())
577            return;
578
579        Map<String, TagModel> modelTags = new HashMap<>();
580        for (int i = 0; i < getRowCount(); i++) {
581            TagModel tagModel = get(i);
582            modelTags.put(tagModel.getName(), tagModel);
583        }
584        for (Tag tag: tags) {
585            TagModel existing = modelTags.get(tag.getKey());
586
587            if (tag.getValue().isEmpty()) {
588                if (existing != null) {
589                    delete(tag.getKey());
590                }
591            } else {
592                if (existing != null) {
593                    updateTagValue(existing, tag.getValue());
594                } else {
595                    add(tag.getKey(), tag.getValue());
596                }
597            }
598        }
599    }
600
601    /**
602     * replies true, if this model has been updated
603     *
604     * @return true, if this model has been updated
605     */
606    public boolean isDirty() {
607        return dirty;
608    }
609
610    class SelectionStateMemento {
611        private int rowMin;
612        private int rowMax;
613        private int colMin;
614        private int colMax;
615
616        SelectionStateMemento() {
617            rowMin = rowSelectionModel.getMinSelectionIndex();
618            rowMax = rowSelectionModel.getMaxSelectionIndex();
619            colMin = colSelectionModel.getMinSelectionIndex();
620            colMax = colSelectionModel.getMaxSelectionIndex();
621        }
622
623        public void apply() {
624            rowSelectionModel.setValueIsAdjusting(true);
625            colSelectionModel.setValueIsAdjusting(true);
626            if (rowMin >= 0 && rowMax >= 0) {
627                rowSelectionModel.setSelectionInterval(rowMin, rowMax);
628            }
629            if (colMin >= 0 && colMax >= 0) {
630                colSelectionModel.setSelectionInterval(colMin, colMax);
631            }
632            rowSelectionModel.setValueIsAdjusting(false);
633            colSelectionModel.setValueIsAdjusting(false);
634        }
635    }
636}