source: josm/trunk/src/org/openstreetmap/josm/gui/tagging/TagEditorModel.java@ 10611

Last change on this file since 10611 was 10611, checked in by Don-vip, 8 years ago

see #11390 - sonar - squid:S1604 - Java 8: Anonymous inner classes containing only one method should become lambdas

  • Property svn:eol-style set to native
File size: 21.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.tagging;
3
4import static org.openstreetmap.josm.tools.I18n.trn;
5
6import java.beans.PropertyChangeListener;
7import java.beans.PropertyChangeSupport;
8import java.util.ArrayList;
9import java.util.Collection;
10import java.util.Collections;
11import java.util.EnumSet;
12import java.util.HashMap;
13import java.util.Iterator;
14import java.util.List;
15import java.util.Map;
16import java.util.Map.Entry;
17
18import javax.swing.DefaultListSelectionModel;
19import javax.swing.table.AbstractTableModel;
20
21import org.openstreetmap.josm.command.ChangePropertyCommand;
22import org.openstreetmap.josm.command.Command;
23import org.openstreetmap.josm.command.SequenceCommand;
24import org.openstreetmap.josm.data.osm.OsmPrimitive;
25import org.openstreetmap.josm.data.osm.Tag;
26import org.openstreetmap.josm.data.osm.TagCollection;
27import org.openstreetmap.josm.data.osm.TagMap;
28import org.openstreetmap.josm.data.osm.Tagged;
29import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
30import org.openstreetmap.josm.tools.CheckParameterUtil;
31
32/**
33 * TagEditorModel is a table model to use with {@link TagEditorPanel}.
34 * @since 1762
35 */
36public class TagEditorModel extends AbstractTableModel {
37 public static final String PROP_DIRTY = TagEditorModel.class.getName() + ".dirty";
38
39 /** the list holding the tags */
40 protected final transient List<TagModel> tags = new ArrayList<>();
41
42 /** indicates whether the model is dirty */
43 private boolean dirty;
44 private final PropertyChangeSupport propChangeSupport = new PropertyChangeSupport(this);
45
46 private final DefaultListSelectionModel rowSelectionModel;
47 private final DefaultListSelectionModel colSelectionModel;
48
49 private transient OsmPrimitive primitive;
50
51 private EndEditListener endEditListener;
52
53 /**
54 * Creates a new tag editor model. Internally allocates two selection models
55 * for row selection and column selection.
56 *
57 * To create a {@link javax.swing.JTable} with this model:
58 * <pre>
59 * TagEditorModel model = new TagEditorModel();
60 * TagTable tbl = new TagTabel(model);
61 * </pre>
62 *
63 * @see #getRowSelectionModel()
64 * @see #getColumnSelectionModel()
65 */
66 public TagEditorModel() {
67 this(new DefaultListSelectionModel(), new DefaultListSelectionModel());
68 }
69
70 /**
71 * Creates a new tag editor model.
72 *
73 * @param rowSelectionModel the row selection model. Must not be null.
74 * @param colSelectionModel the column selection model. Must not be null.
75 * @throws IllegalArgumentException if {@code rowSelectionModel} is null
76 * @throws IllegalArgumentException if {@code colSelectionModel} is null
77 */
78 public TagEditorModel(DefaultListSelectionModel rowSelectionModel, DefaultListSelectionModel colSelectionModel) {
79 CheckParameterUtil.ensureParameterNotNull(rowSelectionModel, "rowSelectionModel");
80 CheckParameterUtil.ensureParameterNotNull(colSelectionModel, "colSelectionModel");
81 this.rowSelectionModel = rowSelectionModel;
82 this.colSelectionModel = colSelectionModel;
83 }
84
85 /**
86 * Adds property change listener.
87 * @param listener property change listener to add
88 */
89 public void addPropertyChangeListener(PropertyChangeListener listener) {
90 propChangeSupport.addPropertyChangeListener(listener);
91 }
92
93 /**
94 * Replies the row selection model used by this tag editor model
95 *
96 * @return the row selection model used by this tag editor model
97 */
98 public DefaultListSelectionModel getRowSelectionModel() {
99 return rowSelectionModel;
100 }
101
102 /**
103 * Replies the column selection model used by this tag editor model
104 *
105 * @return the column selection model used by this tag editor model
106 */
107 public DefaultListSelectionModel getColumnSelectionModel() {
108 return colSelectionModel;
109 }
110
111 /**
112 * Removes property change listener.
113 * @param listener property change listener to remove
114 */
115 public void removePropertyChangeListener(PropertyChangeListener listener) {
116 propChangeSupport.removePropertyChangeListener(listener);
117 }
118
119 protected void fireDirtyStateChanged(final boolean oldValue, final boolean newValue) {
120 propChangeSupport.firePropertyChange(PROP_DIRTY, oldValue, newValue);
121 }
122
123 protected void setDirty(boolean newValue) {
124 boolean oldValue = dirty;
125 dirty = newValue;
126 if (oldValue != newValue) {
127 fireDirtyStateChanged(oldValue, newValue);
128 }
129 }
130
131 @Override
132 public int getColumnCount() {
133 return 2;
134 }
135
136 @Override
137 public int getRowCount() {
138 return tags.size();
139 }
140
141 @Override
142 public Object getValueAt(int rowIndex, int columnIndex) {
143 if (rowIndex >= getRowCount())
144 throw new IndexOutOfBoundsException("unexpected rowIndex: rowIndex=" + rowIndex);
145
146 return tags.get(rowIndex);
147 }
148
149 @Override
150 public void setValueAt(Object value, int row, int col) {
151 TagModel tag = get(row);
152 if (tag != null) {
153 switch(col) {
154 case 0:
155 updateTagName(tag, (String) value);
156 break;
157 case 1:
158 String v = (String) value;
159 if ((tag.getValueCount() > 1 && !v.isEmpty()) || tag.getValueCount() <= 1) {
160 updateTagValue(tag, v);
161 }
162 break;
163 default: // Do nothing
164 }
165 }
166 }
167
168 /**
169 * removes all tags in the model
170 */
171 public void clear() {
172 commitPendingEdit();
173 boolean wasEmpty = tags.isEmpty();
174 tags.clear();
175 if (!wasEmpty) {
176 setDirty(true);
177 fireTableDataChanged();
178 }
179 }
180
181 /**
182 * adds a tag to the model
183 *
184 * @param tag the tag. Must not be null.
185 *
186 * @throws IllegalArgumentException if tag is null
187 */
188 public void add(TagModel tag) {
189 commitPendingEdit();
190 CheckParameterUtil.ensureParameterNotNull(tag, "tag");
191 tags.add(tag);
192 setDirty(true);
193 fireTableDataChanged();
194 }
195
196 /**
197 * Add a tag at the beginning of the table.
198 *
199 * @param tag The tag to add
200 *
201 * @throws IllegalArgumentException if tag is null
202 *
203 * @see #add(TagModel)
204 */
205 public void prepend(TagModel tag) {
206 commitPendingEdit();
207 CheckParameterUtil.ensureParameterNotNull(tag, "tag");
208 tags.add(0, tag);
209 setDirty(true);
210 fireTableDataChanged();
211 }
212
213 /**
214 * adds a tag given by a name/value pair to the tag editor model.
215 *
216 * If there is no tag with name <code>name</code> yet, a new {@link TagModel} is created
217 * and append to this model.
218 *
219 * If there is a tag with name <code>name</code>, <code>value</code> is merged to the list
220 * of values for this tag.
221 *
222 * @param name the name; converted to "" if null
223 * @param value the value; converted to "" if null
224 */
225 public void add(String name, String value) {
226 commitPendingEdit();
227 String key = (name == null) ? "" : name;
228 String val = (value == null) ? "" : value;
229
230 TagModel tag = get(key);
231 if (tag == null) {
232 tag = new TagModel(key, val);
233 int index = tags.size();
234 while (index >= 1 && tags.get(index - 1).getName().isEmpty() && tags.get(index - 1).getValue().isEmpty()) {
235 index--; // If last line(s) is empty, add new tag before it
236 }
237 tags.add(index, tag);
238 } else {
239 tag.addValue(val);
240 }
241 setDirty(true);
242 fireTableDataChanged();
243 }
244
245 /**
246 * replies the tag with name <code>name</code>; null, if no such tag exists
247 * @param name the tag name
248 * @return the tag with name <code>name</code>; null, if no such tag exists
249 */
250 public TagModel get(String name) {
251 String key = (name == null) ? "" : name;
252 for (TagModel tag : tags) {
253 if (tag.getName().equals(key))
254 return tag;
255 }
256 return null;
257 }
258
259 public TagModel get(int idx) {
260 return idx >= tags.size() ? null : tags.get(idx);
261 }
262
263 @Override
264 public boolean isCellEditable(int row, int col) {
265 // all cells are editable
266 return true;
267 }
268
269 /**
270 * deletes the names of the tags given by tagIndices
271 *
272 * @param tagIndices a list of tag indices
273 */
274 public void deleteTagNames(int[] tagIndices) {
275 if (tags == null)
276 return;
277 commitPendingEdit();
278 for (int tagIdx : tagIndices) {
279 TagModel tag = tags.get(tagIdx);
280 if (tag != null) {
281 tag.setName("");
282 }
283 }
284 fireTableDataChanged();
285 setDirty(true);
286 }
287
288 /**
289 * deletes the values of the tags given by tagIndices
290 *
291 * @param tagIndices the lit of tag indices
292 */
293 public void deleteTagValues(int[] tagIndices) {
294 if (tags == null)
295 return;
296 commitPendingEdit();
297 for (int tagIdx : tagIndices) {
298 TagModel tag = tags.get(tagIdx);
299 if (tag != null) {
300 tag.setValue("");
301 }
302 }
303 fireTableDataChanged();
304 setDirty(true);
305 }
306
307 /**
308 * Deletes all tags with name <code>name</code>
309 *
310 * @param name the name. Ignored if null.
311 */
312 public void delete(String name) {
313 commitPendingEdit();
314 if (name == null)
315 return;
316 Iterator<TagModel> it = tags.iterator();
317 boolean changed = false;
318 while (it.hasNext()) {
319 TagModel tm = it.next();
320 if (tm.getName().equals(name)) {
321 changed = true;
322 it.remove();
323 }
324 }
325 if (changed) {
326 fireTableDataChanged();
327 setDirty(true);
328 }
329 }
330
331 /**
332 * deletes the tags given by tagIndices
333 *
334 * @param tagIndices the list of tag indices
335 */
336 public void deleteTags(int[] tagIndices) {
337 if (tags == null)
338 return;
339 commitPendingEdit();
340 List<TagModel> toDelete = new ArrayList<>();
341 for (int tagIdx : tagIndices) {
342 TagModel tag = tags.get(tagIdx);
343 if (tag != null) {
344 toDelete.add(tag);
345 }
346 }
347 for (TagModel tag : toDelete) {
348 tags.remove(tag);
349 }
350 fireTableDataChanged();
351 setDirty(true);
352 }
353
354 /**
355 * creates a new tag and appends it to the model
356 */
357 public void appendNewTag() {
358 TagModel tag = new TagModel();
359 tags.add(tag);
360 fireTableDataChanged();
361 }
362
363 /**
364 * makes sure the model includes at least one (empty) tag
365 */
366 public void ensureOneTag() {
367 if (tags.isEmpty()) {
368 appendNewTag();
369 }
370 }
371
372 /**
373 * initializes the model with the tags of an OSM primitive
374 *
375 * @param primitive the OSM primitive
376 */
377 public void initFromPrimitive(Tagged primitive) {
378 commitPendingEdit();
379 this.tags.clear();
380 for (String key : primitive.keySet()) {
381 String value = primitive.get(key);
382 this.tags.add(new TagModel(key, value));
383 }
384 sort();
385 TagModel tag = new TagModel();
386 tags.add(tag);
387 setDirty(false);
388 fireTableDataChanged();
389 }
390
391 /**
392 * Initializes the model with the tags of an OSM primitive
393 *
394 * @param tags the tags of an OSM primitive
395 */
396 public void initFromTags(Map<String, String> tags) {
397 commitPendingEdit();
398 this.tags.clear();
399 for (Entry<String, String> entry : tags.entrySet()) {
400 this.tags.add(new TagModel(entry.getKey(), entry.getValue()));
401 }
402 sort();
403 TagModel tag = new TagModel();
404 this.tags.add(tag);
405 setDirty(false);
406 }
407
408 /**
409 * Initializes the model with the tags in a tag collection. Removes
410 * all tags if {@code tags} is null.
411 *
412 * @param tags the tags
413 */
414 public void initFromTags(TagCollection tags) {
415 commitPendingEdit();
416 this.tags.clear();
417 if (tags == null) {
418 setDirty(false);
419 return;
420 }
421 for (String key : tags.getKeys()) {
422 String value = tags.getJoinedValues(key);
423 this.tags.add(new TagModel(key, value));
424 }
425 sort();
426 // add an empty row
427 TagModel tag = new TagModel();
428 this.tags.add(tag);
429 setDirty(false);
430 }
431
432 /**
433 * applies the current state of the tag editor model to a primitive
434 *
435 * @param primitive the primitive
436 *
437 */
438 public void applyToPrimitive(Tagged primitive) {
439 primitive.setKeys(applyToTags(false));
440 }
441
442 /**
443 * applies the current state of the tag editor model to a map of tags
444 * @param keepEmpty {@code true} to keep empty tags
445 *
446 * @return the map of key/value pairs
447 */
448 private Map<String, String> applyToTags(boolean keepEmpty) {
449 // TagMap preserves the order of tags.
450 TagMap result = new TagMap();
451 for (TagModel tag: this.tags) {
452 // tag still holds an unchanged list of different values for the same key.
453 // no property change command required
454 if (tag.getValueCount() > 1) {
455 continue;
456 }
457
458 // tag name holds an empty key. Don't apply it to the selection.
459 if (!keepEmpty && (tag.getName().trim().isEmpty() || tag.getValue().trim().isEmpty())) {
460 continue;
461 }
462 result.put(tag.getName().trim(), tag.getValue().trim());
463 }
464 return result;
465 }
466
467 /**
468 * Returns tags, without empty ones.
469 * @return not-empty tags
470 */
471 public Map<String, String> getTags() {
472 return getTags(false);
473 }
474
475 /**
476 * Returns tags.
477 * @param keepEmpty {@code true} to keep empty tags
478 * @return tags
479 */
480 public Map<String, String> getTags(boolean keepEmpty) {
481 return applyToTags(keepEmpty);
482 }
483
484 /**
485 * Replies the tags in this tag editor model as {@link TagCollection}.
486 *
487 * @return the tags in this tag editor model as {@link TagCollection}
488 */
489 public TagCollection getTagCollection() {
490 return TagCollection.from(getTags());
491 }
492
493 /**
494 * checks whether the tag model includes a tag with a given key
495 *
496 * @param key the key
497 * @return true, if the tag model includes the tag; false, otherwise
498 */
499 public boolean includesTag(String key) {
500 if (key != null) {
501 for (TagModel tag : tags) {
502 if (tag.getName().equals(key))
503 return true;
504 }
505 }
506 return false;
507 }
508
509 protected Command createUpdateTagCommand(Collection<OsmPrimitive> primitives, TagModel tag) {
510
511 // tag still holds an unchanged list of different values for the same key.
512 // no property change command required
513 if (tag.getValueCount() > 1)
514 return null;
515
516 // tag name holds an empty key. Don't apply it to the selection.
517 //
518 if (tag.getName().trim().isEmpty())
519 return null;
520
521 return new ChangePropertyCommand(primitives, tag.getName(), tag.getValue());
522 }
523
524 protected Command createDeleteTagsCommand(Collection<OsmPrimitive> primitives) {
525
526 List<String> currentkeys = getKeys();
527 List<Command> commands = new ArrayList<>();
528
529 for (OsmPrimitive prim : primitives) {
530 for (String oldkey : prim.keySet()) {
531 if (!currentkeys.contains(oldkey)) {
532 ChangePropertyCommand deleteCommand =
533 new ChangePropertyCommand(prim, oldkey, null);
534 commands.add(deleteCommand);
535 }
536 }
537 }
538
539 return new SequenceCommand(
540 trn("Remove old keys from up to {0} object", "Remove old keys from up to {0} objects", primitives.size(), primitives.size()),
541 commands
542 );
543 }
544
545 /**
546 * replies the list of keys of the tags managed by this model
547 *
548 * @return the list of keys managed by this model
549 */
550 public List<String> getKeys() {
551 List<String> keys = new ArrayList<>();
552 for (TagModel tag: tags) {
553 if (!tag.getName().trim().isEmpty()) {
554 keys.add(tag.getName());
555 }
556 }
557 return keys;
558 }
559
560 /**
561 * sorts the current tags according alphabetical order of names
562 */
563 protected void sort() {
564 Collections.sort(tags, (self, other) -> self.getName().compareTo(other.getName()));
565 }
566
567 /**
568 * updates the name of a tag and sets the dirty state to true if
569 * the new name is different from the old name.
570 *
571 * @param tag the tag
572 * @param newName the new name
573 */
574 public void updateTagName(TagModel tag, String newName) {
575 String oldName = tag.getName();
576 tag.setName(newName);
577 if (!newName.equals(oldName)) {
578 setDirty(true);
579 }
580 SelectionStateMemento memento = new SelectionStateMemento();
581 fireTableDataChanged();
582 memento.apply();
583 }
584
585 /**
586 * updates the value value of a tag and sets the dirty state to true if the
587 * new name is different from the old name
588 *
589 * @param tag the tag
590 * @param newValue the new value
591 */
592 public void updateTagValue(TagModel tag, String newValue) {
593 String oldValue = tag.getValue();
594 tag.setValue(newValue);
595 if (!newValue.equals(oldValue)) {
596 setDirty(true);
597 }
598 SelectionStateMemento memento = new SelectionStateMemento();
599 fireTableDataChanged();
600 memento.apply();
601 }
602
603 /**
604 * Load tags from given list
605 * @param tags - the list
606 */
607 public void updateTags(List<Tag> tags) {
608 if (tags.isEmpty())
609 return;
610
611 commitPendingEdit();
612 Map<String, TagModel> modelTags = new HashMap<>();
613 for (int i = 0; i < getRowCount(); i++) {
614 TagModel tagModel = get(i);
615 modelTags.put(tagModel.getName(), tagModel);
616 }
617 for (Tag tag: tags) {
618 TagModel existing = modelTags.get(tag.getKey());
619
620 if (tag.getValue().isEmpty()) {
621 if (existing != null) {
622 delete(tag.getKey());
623 }
624 } else {
625 if (existing != null) {
626 updateTagValue(existing, tag.getValue());
627 } else {
628 add(tag.getKey(), tag.getValue());
629 }
630 }
631 }
632 }
633
634 /**
635 * replies true, if this model has been updated
636 *
637 * @return true, if this model has been updated
638 */
639 public boolean isDirty() {
640 return dirty;
641 }
642
643 /**
644 * Returns the list of tagging presets types to consider when updating the presets list panel.
645 * By default returns type of associated primitive or empty set.
646 * @return the list of tagging presets types to consider when updating the presets list panel
647 * @see #forPrimitive
648 * @see TaggingPresetType#forPrimitive
649 * @since 9588
650 */
651 public Collection<TaggingPresetType> getTaggingPresetTypes() {
652 return primitive == null ? EnumSet.noneOf(TaggingPresetType.class) : EnumSet.of(TaggingPresetType.forPrimitive(primitive));
653 }
654
655 /**
656 * Makes this TagEditorModel specific to a given OSM primitive.
657 * @param primitive primitive to consider
658 * @return {@code this}
659 * @since 9588
660 */
661 public TagEditorModel forPrimitive(OsmPrimitive primitive) {
662 this.primitive = primitive;
663 return this;
664 }
665
666 /**
667 * Sets the listener that is notified when an edit should be aborted.
668 * @param endEditListener The listener to be notified when editing should be aborted.
669 */
670 public void setEndEditListener(EndEditListener endEditListener) {
671 this.endEditListener = endEditListener;
672 }
673
674 private void commitPendingEdit() {
675 if (endEditListener != null) {
676 endEditListener.endCellEditing();
677 }
678 }
679
680 class SelectionStateMemento {
681 private final int rowMin;
682 private final int rowMax;
683 private final int colMin;
684 private final int colMax;
685
686 SelectionStateMemento() {
687 rowMin = rowSelectionModel.getMinSelectionIndex();
688 rowMax = rowSelectionModel.getMaxSelectionIndex();
689 colMin = colSelectionModel.getMinSelectionIndex();
690 colMax = colSelectionModel.getMaxSelectionIndex();
691 }
692
693 void apply() {
694 rowSelectionModel.setValueIsAdjusting(true);
695 colSelectionModel.setValueIsAdjusting(true);
696 if (rowMin >= 0 && rowMax >= 0) {
697 rowSelectionModel.setSelectionInterval(rowMin, rowMax);
698 }
699 if (colMin >= 0 && colMax >= 0) {
700 colSelectionModel.setSelectionInterval(colMin, colMax);
701 }
702 rowSelectionModel.setValueIsAdjusting(false);
703 colSelectionModel.setValueIsAdjusting(false);
704 }
705 }
706
707 /**
708 * A listener that is called whenever the cells may be updated from outside the editor and the editor should thus be commited.
709 * @since 10604
710 */
711 @FunctionalInterface
712 public interface EndEditListener {
713 /**
714 * Requests to end the editing of any cells on this model
715 */
716 void endCellEditing();
717 }
718}
Note: See TracBrowser for help on using the repository browser.