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 }