// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.tagging; import static org.openstreetmap.josm.tools.I18n.trn; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.swing.DefaultListSelectionModel; import javax.swing.table.AbstractTableModel; import org.openstreetmap.josm.command.ChangePropertyCommand; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.command.SequenceCommand; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.Tag; import org.openstreetmap.josm.data.osm.TagCollection; import org.openstreetmap.josm.data.osm.Tagged; import org.openstreetmap.josm.tools.CheckParameterUtil; /** * TagEditorModel is a table model. * */ public class TagEditorModel extends AbstractTableModel { public static final String PROP_DIRTY = TagEditorModel.class.getName() + ".dirty"; /** the list holding the tags */ protected final transient List tags = new ArrayList<>(); /** indicates whether the model is dirty */ private boolean dirty; private final PropertyChangeSupport propChangeSupport = new PropertyChangeSupport(this); private DefaultListSelectionModel rowSelectionModel; private DefaultListSelectionModel colSelectionModel; /** * Creates a new tag editor model. Internally allocates two selection models * for row selection and column selection. * * To create a {@link javax.swing.JTable} with this model: *
     *    TagEditorModel model = new TagEditorModel();
     *    TagTable tbl  = new TagTabel(model);
     * 
* * @see #getRowSelectionModel() * @see #getColumnSelectionModel() */ public TagEditorModel() { this.rowSelectionModel = new DefaultListSelectionModel(); this.colSelectionModel = new DefaultListSelectionModel(); } /** * Creates a new tag editor model. * * @param rowSelectionModel the row selection model. Must not be null. * @param colSelectionModel the column selection model. Must not be null. * @throws IllegalArgumentException if {@code rowSelectionModel} is null * @throws IllegalArgumentException if {@code colSelectionModel} is null */ public TagEditorModel(DefaultListSelectionModel rowSelectionModel, DefaultListSelectionModel colSelectionModel) { CheckParameterUtil.ensureParameterNotNull(rowSelectionModel, "rowSelectionModel"); CheckParameterUtil.ensureParameterNotNull(colSelectionModel, "colSelectionModel"); this.rowSelectionModel = rowSelectionModel; this.colSelectionModel = colSelectionModel; } public void addPropertyChangeListener(PropertyChangeListener listener) { propChangeSupport.addPropertyChangeListener(listener); } /** * Replies the row selection model used by this tag editor model * * @return the row selection model used by this tag editor model */ public DefaultListSelectionModel getRowSelectionModel() { return rowSelectionModel; } /** * Replies the column selection model used by this tag editor model * * @return the column selection model used by this tag editor model */ public DefaultListSelectionModel getColumnSelectionModel() { return colSelectionModel; } public void removeProperyChangeListener(PropertyChangeListener listener) { propChangeSupport.removePropertyChangeListener(listener); } protected void fireDirtyStateChanged(final boolean oldValue, final boolean newValue) { propChangeSupport.firePropertyChange(PROP_DIRTY, oldValue, newValue); } protected void setDirty(boolean newValue) { boolean oldValue = dirty; dirty = newValue; if (oldValue != newValue) { fireDirtyStateChanged(oldValue, newValue); } } @Override public int getColumnCount() { return 2; } @Override public int getRowCount() { return tags.size(); } @Override public Object getValueAt(int rowIndex, int columnIndex) { if (rowIndex >= getRowCount()) throw new IndexOutOfBoundsException("unexpected rowIndex: rowIndex=" + rowIndex); TagModel tag = tags.get(rowIndex); switch(columnIndex) { case 0: case 1: return tag; default: throw new IndexOutOfBoundsException("unexpected columnIndex: columnIndex=" + columnIndex); } } @Override public void setValueAt(Object value, int row, int col) { TagModel tag = get(row); if (tag == null) return; switch(col) { case 0: updateTagName(tag, (String) value); break; case 1: String v = (String) value; if (tag.getValueCount() > 1 && !v.isEmpty()) { updateTagValue(tag, v); } else if (tag.getValueCount() <= 1) { updateTagValue(tag, v); } } } /** * removes all tags in the model */ public void clear() { tags.clear(); setDirty(true); fireTableDataChanged(); } /** * adds a tag to the model * * @param tag the tag. Must not be null. * * @throws IllegalArgumentException if tag is null */ public void add(TagModel tag) { CheckParameterUtil.ensureParameterNotNull(tag, "tag"); tags.add(tag); setDirty(true); fireTableDataChanged(); } public void prepend(TagModel tag) { CheckParameterUtil.ensureParameterNotNull(tag, "tag"); tags.add(0, tag); setDirty(true); fireTableDataChanged(); } /** * adds a tag given by a name/value pair to the tag editor model. * * If there is no tag with name name yet, a new {@link TagModel} is created * and append to this model. * * If there is a tag with name name, value is merged to the list * of values for this tag. * * @param name the name; converted to "" if null * @param value the value; converted to "" if null */ public void add(String name, String value) { name = (name == null) ? "" : name; value = (value == null) ? "" : value; TagModel tag = get(name); if (tag == null) { tag = new TagModel(name, value); int index = tags.size(); while (index >= 1 && tags.get(index - 1).getName().isEmpty() && tags.get(index - 1).getValue().isEmpty()) { index--; // If last line(s) is empty, add new tag before it } tags.add(index, tag); } else { tag.addValue(value); } setDirty(true); fireTableDataChanged(); } /** * replies the tag with name name; null, if no such tag exists * @param name the tag name * @return the tag with name name; null, if no such tag exists */ public TagModel get(String name) { name = (name == null) ? "" : name; for (TagModel tag : tags) { if (tag.getName().equals(name)) return tag; } return null; } public TagModel get(int idx) { if (idx >= tags.size()) return null; return tags.get(idx); } @Override public boolean isCellEditable(int row, int col) { // all cells are editable return true; } /** * deletes the names of the tags given by tagIndices * * @param tagIndices a list of tag indices */ public void deleteTagNames(int[] tagIndices) { if (tags == null) return; for (int tagIdx : tagIndices) { TagModel tag = tags.get(tagIdx); if (tag != null) { tag.setName(""); } } fireTableDataChanged(); setDirty(true); } /** * deletes the values of the tags given by tagIndices * * @param tagIndices the lit of tag indices */ public void deleteTagValues(int[] tagIndices) { if (tags == null) return; for (int tagIdx : tagIndices) { TagModel tag = tags.get(tagIdx); if (tag != null) { tag.setValue(""); } } fireTableDataChanged(); setDirty(true); } /** * Deletes all tags with name name * * @param name the name. Ignored if null. */ public void delete(String name) { if (name == null) return; Iterator it = tags.iterator(); boolean changed = false; while (it.hasNext()) { TagModel tm = it.next(); if (tm.getName().equals(name)) { changed = true; it.remove(); } } if (changed) { fireTableDataChanged(); setDirty(true); } } /** * deletes the tags given by tagIndices * * @param tagIndices the list of tag indices */ public void deleteTags(int[] tagIndices) { if (tags == null) return; List toDelete = new ArrayList<>(); for (int tagIdx : tagIndices) { TagModel tag = tags.get(tagIdx); if (tag != null) { toDelete.add(tag); } } for (TagModel tag : toDelete) { tags.remove(tag); } fireTableDataChanged(); setDirty(true); } /** * creates a new tag and appends it to the model */ public void appendNewTag() { TagModel tag = new TagModel(); tags.add(tag); fireTableDataChanged(); setDirty(true); } /** * makes sure the model includes at least one (empty) tag */ public void ensureOneTag() { if (tags.isEmpty()) { appendNewTag(); } } /** * initializes the model with the tags of an OSM primitive * * @param primitive the OSM primitive */ public void initFromPrimitive(Tagged primitive) { this.tags.clear(); for (String key : primitive.keySet()) { String value = primitive.get(key); this.tags.add(new TagModel(key, value)); } TagModel tag = new TagModel(); sort(); tags.add(tag); setDirty(false); fireTableDataChanged(); } /** * Initializes the model with the tags of an OSM primitive * * @param tags the tags of an OSM primitive */ public void initFromTags(Map tags) { this.tags.clear(); for (Entry entry : tags.entrySet()) { this.tags.add(new TagModel(entry.getKey(), entry.getValue())); } sort(); TagModel tag = new TagModel(); this.tags.add(tag); setDirty(false); } /** * Initializes the model with the tags in a tag collection. Removes * all tags if {@code tags} is null. * * @param tags the tags */ public void initFromTags(TagCollection tags) { this.tags.clear(); if (tags == null) { setDirty(false); return; } for (String key : tags.getKeys()) { String value = tags.getJoinedValues(key); this.tags.add(new TagModel(key, value)); } sort(); // add an empty row TagModel tag = new TagModel(); this.tags.add(tag); setDirty(false); } /** * applies the current state of the tag editor model to a primitive * * @param primitive the primitive * */ public void applyToPrimitive(Tagged primitive) { primitive.setKeys(applyToTags(false)); } /** * applies the current state of the tag editor model to a map of tags * * @return the map of key/value pairs */ private Map applyToTags(boolean keepEmpty) { Map result = new HashMap<>(); for (TagModel tag: this.tags) { // tag still holds an unchanged list of different values for the same key. // no property change command required if (tag.getValueCount() > 1) { continue; } // tag name holds an empty key. Don't apply it to the selection. // if (!keepEmpty && (tag.getName().trim().isEmpty() || tag.getValue().trim().isEmpty())) { continue; } result.put(tag.getName().trim(), tag.getValue().trim()); } return result; } public Map getTags() { return getTags(false); } public Map getTags(boolean keepEmpty) { return applyToTags(keepEmpty); } /** * Replies the tags in this tag editor model as {@link TagCollection}. * * @return the tags in this tag editor model as {@link TagCollection} */ public TagCollection getTagCollection() { return TagCollection.from(getTags()); } /** * checks whether the tag model includes a tag with a given key * * @param key the key * @return true, if the tag model includes the tag; false, otherwise */ public boolean includesTag(String key) { if (key == null) return false; for (TagModel tag : tags) { if (tag.getName().equals(key)) return true; } return false; } protected Command createUpdateTagCommand(Collection primitives, TagModel tag) { // tag still holds an unchanged list of different values for the same key. // no property change command required if (tag.getValueCount() > 1) return null; // tag name holds an empty key. Don't apply it to the selection. // if (tag.getName().trim().isEmpty()) return null; return new ChangePropertyCommand(primitives, tag.getName(), tag.getValue()); } protected Command createDeleteTagsCommand(Collection primitives) { List currentkeys = getKeys(); List commands = new ArrayList<>(); for (OsmPrimitive primitive : primitives) { for (String oldkey : primitive.keySet()) { if (!currentkeys.contains(oldkey)) { ChangePropertyCommand deleteCommand = new ChangePropertyCommand(primitive, oldkey, null); commands.add(deleteCommand); } } } return new SequenceCommand( trn("Remove old keys from up to {0} object", "Remove old keys from up to {0} objects", primitives.size(), primitives.size()), commands ); } /** * replies the list of keys of the tags managed by this model * * @return the list of keys managed by this model */ public List getKeys() { List keys = new ArrayList<>(); for (TagModel tag: tags) { if (!tag.getName().trim().isEmpty()) { keys.add(tag.getName()); } } return keys; } /** * sorts the current tags according alphabetical order of names */ protected void sort() { java.util.Collections.sort( tags, new Comparator() { @Override public int compare(TagModel self, TagModel other) { return self.getName().compareTo(other.getName()); } } ); } /** * updates the name of a tag and sets the dirty state to true if * the new name is different from the old name. * * @param tag the tag * @param newName the new name */ public void updateTagName(TagModel tag, String newName) { String oldName = tag.getName(); tag.setName(newName); if (!newName.equals(oldName)) { setDirty(true); } SelectionStateMemento memento = new SelectionStateMemento(); fireTableDataChanged(); memento.apply(); } /** * updates the value value of a tag and sets the dirty state to true if the * new name is different from the old name * * @param tag the tag * @param newValue the new value */ public void updateTagValue(TagModel tag, String newValue) { String oldValue = tag.getValue(); tag.setValue(newValue); if (!newValue.equals(oldValue)) { setDirty(true); } SelectionStateMemento memento = new SelectionStateMemento(); fireTableDataChanged(); memento.apply(); } /** * Load tags from given list * @param tags - the list */ public void updateTags(List tags) { if (tags.isEmpty()) return; Map modelTags = new HashMap<>(); for (int i = 0; i < getRowCount(); i++) { TagModel tagModel = get(i); modelTags.put(tagModel.getName(), tagModel); } for (Tag tag: tags) { TagModel existing = modelTags.get(tag.getKey()); if (tag.getValue().isEmpty()) { if (existing != null) { delete(tag.getKey()); } } else { if (existing != null) { updateTagValue(existing, tag.getValue()); } else { add(tag.getKey(), tag.getValue()); } } } } /** * replies true, if this model has been updated * * @return true, if this model has been updated */ public boolean isDirty() { return dirty; } class SelectionStateMemento { private int rowMin; private int rowMax; private int colMin; private int colMax; SelectionStateMemento() { rowMin = rowSelectionModel.getMinSelectionIndex(); rowMax = rowSelectionModel.getMaxSelectionIndex(); colMin = colSelectionModel.getMinSelectionIndex(); colMax = colSelectionModel.getMaxSelectionIndex(); } public void apply() { rowSelectionModel.setValueIsAdjusting(true); colSelectionModel.setValueIsAdjusting(true); if (rowMin >= 0 && rowMax >= 0) { rowSelectionModel.setSelectionInterval(rowMin, rowMax); } if (colMin >= 0 && colMax >= 0) { colSelectionModel.setSelectionInterval(colMin, colMax); } rowSelectionModel.setValueIsAdjusting(false); colSelectionModel.setValueIsAdjusting(false); } } }