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 DefaultListSelectionModel rowSelectionModel; 044 private 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 * 416 * @return the map of key/value pairs 417 */ 418 private Map<String, String> applyToTags(boolean keepEmpty) { 419 Map<String, String> result = new HashMap<>(); 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 (!keepEmpty && (tag.getName().trim().isEmpty() || tag.getValue().trim().isEmpty())) { 430 continue; 431 } 432 result.put(tag.getName().trim(), tag.getValue().trim()); 433 } 434 return result; 435 } 436 437 public Map<String, String> getTags() { 438 return getTags(false); 439 } 440 441 public Map<String, String> getTags(boolean keepEmpty) { 442 return applyToTags(keepEmpty); 443 } 444 445 /** 446 * Replies the tags in this tag editor model as {@link TagCollection}. 447 * 448 * @return the tags in this tag editor model as {@link TagCollection} 449 */ 450 public TagCollection getTagCollection() { 451 return TagCollection.from(getTags()); 452 } 453 454 /** 455 * checks whether the tag model includes a tag with a given key 456 * 457 * @param key the key 458 * @return true, if the tag model includes the tag; false, otherwise 459 */ 460 public boolean includesTag(String key) { 461 if (key == null) return false; 462 for (TagModel tag : tags) { 463 if (tag.getName().equals(key)) 464 return true; 465 } 466 return false; 467 } 468 469 protected Command createUpdateTagCommand(Collection<OsmPrimitive> primitives, TagModel tag) { 470 471 // tag still holds an unchanged list of different values for the same key. 472 // no property change command required 473 if (tag.getValueCount() > 1) 474 return null; 475 476 // tag name holds an empty key. Don't apply it to the selection. 477 // 478 if (tag.getName().trim().isEmpty()) 479 return null; 480 481 return new ChangePropertyCommand(primitives, tag.getName(), tag.getValue()); 482 } 483 484 protected Command createDeleteTagsCommand(Collection<OsmPrimitive> primitives) { 485 486 List<String> currentkeys = getKeys(); 487 List<Command> commands = new ArrayList<>(); 488 489 for (OsmPrimitive primitive : primitives) { 490 for (String oldkey : primitive.keySet()) { 491 if (!currentkeys.contains(oldkey)) { 492 ChangePropertyCommand deleteCommand = 493 new ChangePropertyCommand(primitive, oldkey, null); 494 commands.add(deleteCommand); 495 } 496 } 497 } 498 499 return new SequenceCommand( 500 trn("Remove old keys from up to {0} object", "Remove old keys from up to {0} objects", primitives.size(), primitives.size()), 501 commands 502 ); 503 } 504 505 /** 506 * replies the list of keys of the tags managed by this model 507 * 508 * @return the list of keys managed by this model 509 */ 510 public List<String> getKeys() { 511 List<String> keys = new ArrayList<>(); 512 for (TagModel tag: tags) { 513 if (!tag.getName().trim().isEmpty()) { 514 keys.add(tag.getName()); 515 } 516 } 517 return keys; 518 } 519 520 /** 521 * sorts the current tags according alphabetical order of names 522 */ 523 protected void sort() { 524 java.util.Collections.sort( 525 tags, 526 new Comparator<TagModel>() { 527 @Override 528 public int compare(TagModel self, TagModel other) { 529 return self.getName().compareTo(other.getName()); 530 } 531 } 532 ); 533 } 534 535 /** 536 * updates the name of a tag and sets the dirty state to true if 537 * the new name is different from the old name. 538 * 539 * @param tag the tag 540 * @param newName the new name 541 */ 542 public void updateTagName(TagModel tag, String newName) { 543 String oldName = tag.getName(); 544 tag.setName(newName); 545 if (!newName.equals(oldName)) { 546 setDirty(true); 547 } 548 SelectionStateMemento memento = new SelectionStateMemento(); 549 fireTableDataChanged(); 550 memento.apply(); 551 } 552 553 /** 554 * updates the value value of a tag and sets the dirty state to true if the 555 * new name is different from the old name 556 * 557 * @param tag the tag 558 * @param newValue the new value 559 */ 560 public void updateTagValue(TagModel tag, String newValue) { 561 String oldValue = tag.getValue(); 562 tag.setValue(newValue); 563 if (!newValue.equals(oldValue)) { 564 setDirty(true); 565 } 566 SelectionStateMemento memento = new SelectionStateMemento(); 567 fireTableDataChanged(); 568 memento.apply(); 569 } 570 571 /** 572 * Load tags from given list 573 * @param tags - the list 574 */ 575 public void updateTags(List<Tag> tags) { 576 if (tags.isEmpty()) 577 return; 578 579 Map<String, TagModel> modelTags = new HashMap<>(); 580 for (int i = 0; i < getRowCount(); i++) { 581 TagModel tagModel = get(i); 582 modelTags.put(tagModel.getName(), tagModel); 583 } 584 for (Tag tag: tags) { 585 TagModel existing = modelTags.get(tag.getKey()); 586 587 if (tag.getValue().isEmpty()) { 588 if (existing != null) { 589 delete(tag.getKey()); 590 } 591 } else { 592 if (existing != null) { 593 updateTagValue(existing, tag.getValue()); 594 } else { 595 add(tag.getKey(), tag.getValue()); 596 } 597 } 598 } 599 } 600 601 /** 602 * replies true, if this model has been updated 603 * 604 * @return true, if this model has been updated 605 */ 606 public boolean isDirty() { 607 return dirty; 608 } 609 610 class SelectionStateMemento { 611 private int rowMin; 612 private int rowMax; 613 private int colMin; 614 private int colMax; 615 616 SelectionStateMemento() { 617 rowMin = rowSelectionModel.getMinSelectionIndex(); 618 rowMax = rowSelectionModel.getMaxSelectionIndex(); 619 colMin = colSelectionModel.getMinSelectionIndex(); 620 colMax = colSelectionModel.getMaxSelectionIndex(); 621 } 622 623 public void apply() { 624 rowSelectionModel.setValueIsAdjusting(true); 625 colSelectionModel.setValueIsAdjusting(true); 626 if (rowMin >= 0 && rowMax >= 0) { 627 rowSelectionModel.setSelectionInterval(rowMin, rowMax); 628 } 629 if (colMin >= 0 && colMax >= 0) { 630 colSelectionModel.setSelectionInterval(colMin, colMax); 631 } 632 rowSelectionModel.setValueIsAdjusting(false); 633 colSelectionModel.setValueIsAdjusting(false); 634 } 635 } 636}