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 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 * 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 ArrayList<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 setDirty(true); 334 } 335 336 /** 337 * makes sure the model includes at least one (empty) tag 338 */ 339 public void ensureOneTag() { 340 if (tags.isEmpty()) { 341 appendNewTag(); 342 } 343 } 344 345 /** 346 * initializes the model with the tags of an OSM primitive 347 * 348 * @param primitive the OSM primitive 349 */ 350 public void initFromPrimitive(Tagged primitive) { 351 this.tags.clear(); 352 for (String key : primitive.keySet()) { 353 String value = primitive.get(key); 354 this.tags.add(new TagModel(key,value)); 355 } 356 TagModel tag = new TagModel(); 357 sort(); 358 tags.add(tag); 359 setDirty(false); 360 fireTableDataChanged(); 361 } 362 363 /** 364 * Initializes the model with the tags of an OSM primitive 365 * 366 * @param tags the tags of an OSM primitive 367 */ 368 public void initFromTags(Map<String,String> tags) { 369 this.tags.clear(); 370 for (Entry<String, String> entry : tags.entrySet()) { 371 this.tags.add(new TagModel(entry.getKey(), entry.getValue())); 372 } 373 sort(); 374 TagModel tag = new TagModel(); 375 this.tags.add(tag); 376 setDirty(false); 377 } 378 379 /** 380 * Initializes the model with the tags in a tag collection. Removes 381 * all tags if {@code tags} is null. 382 * 383 * @param tags the tags 384 */ 385 public void initFromTags(TagCollection tags) { 386 this.tags.clear(); 387 if (tags == null){ 388 setDirty(false); 389 return; 390 } 391 for (String key : tags.getKeys()) { 392 String value = tags.getJoinedValues(key); 393 this.tags.add(new TagModel(key,value)); 394 } 395 sort(); 396 // add an empty row 397 TagModel tag = new TagModel(); 398 this.tags.add(tag); 399 setDirty(false); 400 } 401 402 /** 403 * applies the current state of the tag editor model to a primitive 404 * 405 * @param primitive the primitive 406 * 407 */ 408 public void applyToPrimitive(Tagged primitive) { 409 Map<String,String> tags = primitive.getKeys(); 410 applyToTags(tags, false); 411 primitive.setKeys(tags); 412 } 413 414 /** 415 * applies the current state of the tag editor model to a map of tags 416 * 417 * @param tags the map of key/value pairs 418 * 419 */ 420 public void applyToTags(Map<String, String> tags, boolean keepEmpty) { 421 tags.clear(); 422 for (TagModel tag: this.tags) { 423 // tag still holds an unchanged list of different values for the same key. 424 // no property change command required 425 if (tag.getValueCount() > 1) { 426 continue; 427 } 428 429 // tag name holds an empty key. Don't apply it to the selection. 430 // 431 if (!keepEmpty && (tag.getName().trim().isEmpty() || tag.getValue().trim().isEmpty())) { 432 continue; 433 } 434 tags.put(tag.getName().trim(), tag.getValue().trim()); 435 } 436 } 437 438 public Map<String,String> getTags() { 439 return getTags(false); 440 } 441 442 public Map<String,String> getTags(boolean keepEmpty) { 443 Map<String,String> tags = new HashMap<>(); 444 applyToTags(tags, keepEmpty); 445 return tags; 446 } 447 448 /** 449 * Replies the tags in this tag editor model as {@link TagCollection}. 450 * 451 * @return the tags in this tag editor model as {@link TagCollection} 452 */ 453 public TagCollection getTagCollection() { 454 return TagCollection.from(getTags()); 455 } 456 457 /** 458 * checks whether the tag model includes a tag with a given key 459 * 460 * @param key the key 461 * @return true, if the tag model includes the tag; false, otherwise 462 */ 463 public boolean includesTag(String key) { 464 if (key == null) return false; 465 for (TagModel tag : tags) { 466 if (tag.getName().equals(key)) 467 return true; 468 } 469 return false; 470 } 471 472 protected Command createUpdateTagCommand(Collection<OsmPrimitive> primitives, TagModel tag) { 473 474 // tag still holds an unchanged list of different values for the same key. 475 // no property change command required 476 if (tag.getValueCount() > 1) 477 return null; 478 479 // tag name holds an empty key. Don't apply it to the selection. 480 // 481 if (tag.getName().trim().isEmpty()) 482 return null; 483 484 return new ChangePropertyCommand(primitives, tag.getName(), tag.getValue()); 485 } 486 487 protected Command createDeleteTagsCommand(Collection<OsmPrimitive> primitives) { 488 489 List<String> currentkeys = getKeys(); 490 ArrayList<Command> commands = new ArrayList<>(); 491 492 for (OsmPrimitive primitive : primitives) { 493 for (String oldkey : primitive.keySet()) { 494 if (!currentkeys.contains(oldkey)) { 495 ChangePropertyCommand deleteCommand = 496 new ChangePropertyCommand(primitive,oldkey,null); 497 commands.add(deleteCommand); 498 } 499 } 500 } 501 502 return new SequenceCommand( 503 trn("Remove old keys from up to {0} object", "Remove old keys from up to {0} objects", primitives.size(), primitives.size()), 504 commands 505 ); 506 } 507 508 /** 509 * replies the list of keys of the tags managed by this model 510 * 511 * @return the list of keys managed by this model 512 */ 513 public List<String> getKeys() { 514 ArrayList<String> keys = new ArrayList<>(); 515 for (TagModel tag: tags) { 516 if (!tag.getName().trim().isEmpty()) { 517 keys.add(tag.getName()); 518 } 519 } 520 return keys; 521 } 522 523 /** 524 * sorts the current tags according alphabetical order of names 525 */ 526 protected void sort() { 527 java.util.Collections.sort( 528 tags, 529 new Comparator<TagModel>() { 530 @Override 531 public int compare(TagModel self, TagModel other) { 532 return self.getName().compareTo(other.getName()); 533 } 534 } 535 ); 536 } 537 538 /** 539 * updates the name of a tag and sets the dirty state to true if 540 * the new name is different from the old name. 541 * 542 * @param tag the tag 543 * @param newName the new name 544 */ 545 public void updateTagName(TagModel tag, String newName) { 546 String oldName = tag.getName(); 547 tag.setName(newName); 548 if (! newName.equals(oldName)) { 549 setDirty(true); 550 } 551 SelectionStateMemento memento = new SelectionStateMemento(); 552 fireTableDataChanged(); 553 memento.apply(); 554 } 555 556 /** 557 * updates the value value of a tag and sets the dirty state to true if the 558 * new name is different from the old name 559 * 560 * @param tag the tag 561 * @param newValue the new value 562 */ 563 public void updateTagValue(TagModel tag, String newValue) { 564 String oldValue = tag.getValue(); 565 tag.setValue(newValue); 566 if (! newValue.equals(oldValue)) { 567 setDirty(true); 568 } 569 SelectionStateMemento memento = new SelectionStateMemento(); 570 fireTableDataChanged(); 571 memento.apply(); 572 } 573 574 /** 575 * Load tags from given list 576 * @param tags - the list 577 */ 578 public void updateTags(List<Tag> tags) { 579 if (tags.isEmpty()) 580 return; 581 582 Map<String, TagModel> modelTags = new HashMap<>(); 583 for (int i=0; i<getRowCount(); i++) { 584 TagModel tagModel = get(i); 585 modelTags.put(tagModel.getName(), tagModel); 586 } 587 for (Tag tag: tags) { 588 TagModel existing = modelTags.get(tag.getKey()); 589 590 if (tag.getValue().isEmpty()) { 591 if (existing != null) { 592 delete(tag.getKey()); 593 } 594 } else { 595 if (existing != null) { 596 updateTagValue(existing, tag.getValue()); 597 } else { 598 add(tag.getKey(), tag.getValue()); 599 } 600 } 601 } 602 } 603 604 /** 605 * replies true, if this model has been updated 606 * 607 * @return true, if this model has been updated 608 */ 609 public boolean isDirty() { 610 return dirty; 611 } 612 613 class SelectionStateMemento { 614 private int rowMin; 615 private int rowMax; 616 private int colMin; 617 private int colMax; 618 619 public SelectionStateMemento() { 620 rowMin = rowSelectionModel.getMinSelectionIndex(); 621 rowMax = rowSelectionModel.getMaxSelectionIndex(); 622 colMin = colSelectionModel.getMinSelectionIndex(); 623 colMax = colSelectionModel.getMaxSelectionIndex(); 624 } 625 626 public void apply() { 627 rowSelectionModel.setValueIsAdjusting(true); 628 colSelectionModel.setValueIsAdjusting(true); 629 if (rowMin >= 0 && rowMax >=0) { 630 rowSelectionModel.setSelectionInterval(rowMin, rowMax); 631 } 632 if (colMin >=0 && colMax >= 0) { 633 colSelectionModel.setSelectionInterval(colMin, colMax); 634 } 635 rowSelectionModel.setValueIsAdjusting(false); 636 colSelectionModel.setValueIsAdjusting(false); 637 } 638 } 639}