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