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