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 public static final String PROP_DIRTY = TagEditorModel.class.getName() + ".dirty"; 036 037 /** the list holding the tags */ 038 protected final List<TagModel> tags =new ArrayList<>(); 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</code> yet, a new {@link TagModel} is created 197 * and append to this model. 198 * 199 * If there is a tag with name <code>name</code>, <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 return tags.get(idx); 241 } 242 243 @Override 244 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<>(); 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<>(); 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 return new ChangePropertyCommand(primitives, tag.getName(), tag.getValue()); 487 } 488 489 protected Command createDeleteTagsCommand(Collection<OsmPrimitive> primitives) { 490 491 List<String> currentkeys = getKeys(); 492 ArrayList<Command> commands = new ArrayList<>(); 493 494 for (OsmPrimitive primitive : primitives) { 495 for (String oldkey : primitive.keySet()) { 496 if (!currentkeys.contains(oldkey)) { 497 ChangePropertyCommand deleteCommand = 498 new ChangePropertyCommand(primitive,oldkey,null); 499 commands.add(deleteCommand); 500 } 501 } 502 } 503 504 return new SequenceCommand( 505 trn("Remove old keys from up to {0} object", "Remove old keys from up to {0} objects", primitives.size(), primitives.size()), 506 commands 507 ); 508 } 509 510 /** 511 * replies the list of keys of the tags managed by this model 512 * 513 * @return the list of keys managed by this model 514 */ 515 public List<String> getKeys() { 516 ArrayList<String> keys = new ArrayList<>(); 517 for (TagModel tag: tags) { 518 if (!tag.getName().trim().isEmpty()) { 519 keys.add(tag.getName()); 520 } 521 } 522 return keys; 523 } 524 525 /** 526 * sorts the current tags according alphabetical order of names 527 */ 528 protected void sort() { 529 java.util.Collections.sort( 530 tags, 531 new Comparator<TagModel>() { 532 @Override 533 public int compare(TagModel self, TagModel other) { 534 return self.getName().compareTo(other.getName()); 535 } 536 } 537 ); 538 } 539 540 /** 541 * updates the name of a tag and sets the dirty state to true if 542 * the new name is different from the old name. 543 * 544 * @param tag the tag 545 * @param newName the new name 546 */ 547 public void updateTagName(TagModel tag, String newName) { 548 String oldName = tag.getName(); 549 tag.setName(newName); 550 if (! newName.equals(oldName)) { 551 setDirty(true); 552 } 553 SelectionStateMemento memento = new SelectionStateMemento(); 554 fireTableDataChanged(); 555 memento.apply(); 556 } 557 558 /** 559 * updates the value value of a tag and sets the dirty state to true if the 560 * new name is different from the old name 561 * 562 * @param tag the tag 563 * @param newValue the new value 564 */ 565 public void updateTagValue(TagModel tag, String newValue) { 566 String oldValue = tag.getValue(); 567 tag.setValue(newValue); 568 if (! newValue.equals(oldValue)) { 569 setDirty(true); 570 } 571 SelectionStateMemento memento = new SelectionStateMemento(); 572 fireTableDataChanged(); 573 memento.apply(); 574 } 575 576 /** 577 * Load tags from given list 578 * @param tags - the list 579 */ 580 public void updateTags(List<Tag> tags) { 581 if (tags.isEmpty()) 582 return; 583 584 Map<String, TagModel> modelTags = new HashMap<>(); 585 for (int i=0; i<getRowCount(); i++) { 586 TagModel tagModel = get(i); 587 modelTags.put(tagModel.getName(), tagModel); 588 } 589 for (Tag tag: tags) { 590 TagModel existing = modelTags.get(tag.getKey()); 591 592 if (tag.getValue().isEmpty()) { 593 if (existing != null) { 594 delete(tag.getKey()); 595 } 596 } else { 597 if (existing != null) { 598 updateTagValue(existing, tag.getValue()); 599 } else { 600 add(tag.getKey(), tag.getValue()); 601 } 602 } 603 } 604 } 605 606 /** 607 * replies true, if this model has been updated 608 * 609 * @return true, if this model has been updated 610 */ 611 public boolean isDirty() { 612 return dirty; 613 } 614 615 class SelectionStateMemento { 616 private int rowMin; 617 private int rowMax; 618 private int colMin; 619 private int colMax; 620 621 public SelectionStateMemento() { 622 rowMin = rowSelectionModel.getMinSelectionIndex(); 623 rowMax = rowSelectionModel.getMaxSelectionIndex(); 624 colMin = colSelectionModel.getMinSelectionIndex(); 625 colMax = colSelectionModel.getMaxSelectionIndex(); 626 } 627 628 public void apply() { 629 rowSelectionModel.setValueIsAdjusting(true); 630 colSelectionModel.setValueIsAdjusting(true); 631 if (rowMin >= 0 && rowMax >=0) { 632 rowSelectionModel.setSelectionInterval(rowMin, rowMax); 633 } 634 if (colMin >=0 && colMax >= 0) { 635 colSelectionModel.setSelectionInterval(colMin, colMax); 636 } 637 rowSelectionModel.setValueIsAdjusting(false); 638 colSelectionModel.setValueIsAdjusting(false); 639 } 640 } 641}