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