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