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