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

Last change on this file since 17356 was 16468, checked in by simon04, 4 years ago

Java 8: use Collection.removeIf

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