Ticket #13467: patch-dataset-selection-listener.patch

File patch-dataset-selection-listener.patch, 28.9 KB (added by michael2402, 2 years ago)
  • new file src/org/openstreetmap/josm/data/osm/DataSelectionListener.java

    diff --git a/src/org/openstreetmap/josm/data/osm/DataSelectionListener.java b/src/org/openstreetmap/josm/data/osm/DataSelectionListener.java
    new file mode 100644
    index 0000000..475d9e8
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.osm;
     3
     4import java.util.Collections;
     5import java.util.HashSet;
     6import java.util.Set;
     7import java.util.stream.Collectors;
     8import java.util.stream.Stream;
     9
     10import org.openstreetmap.josm.tools.CheckParameterUtil;
     11
     12/**
     13 * This is a listener that listens to selection change events in the data set.
     14 * @author Michael Zangl
     15 * @since xxx
     16 */
     17@FunctionalInterface
     18public interface DataSelectionListener {
     19
     20    /**
     21     * Called whenever the selection is changed.
     22     * @param e The selection change event.
     23     */
     24    void selectionChanged(SelectionChangeEvent e);
     25
     26    /**
     27     * The event that is fired when the selection changed.
     28     * @author Michael Zangl
     29     * @since xxx
     30     */
     31    public static interface SelectionChangeEvent {
     32        /**
     33         * Gets the previous selection
     34         * <p>
     35         * This collection cannot be modified and will not change.
     36         * @return The old selection
     37         */
     38        public Set<OsmPrimitive> getOldSelection();
     39
     40        /**
     41         * Gets the new selection
     42         * <p>
     43         * This collection cannot be modified and will not change.
     44         * @return The new selection
     45         */
     46        public Set<OsmPrimitive> getSelection();
     47
     48        /**
     49         * Gets the primitives that have been removed from the selection.
     50         * <p>
     51         * Those are the primitives contained in {@link #getOldSelection()} but not in {@link #getSelection()}
     52         * <p>
     53         * This collection cannot be modified and will not change.
     54         * @return The primitives
     55         */
     56        public Set<OsmPrimitive> getRemoved();
     57
     58        /**
     59         * Gets the primitives that have been added to the selection.
     60         * <p>
     61         * Those are the primitives contained in {@link #getSelection()} but not in {@link #getOldSelection()}
     62         * <p>
     63         * This collection cannot be modified and will not change.
     64         * @return The primitives
     65         */
     66        public Set<OsmPrimitive> getAdded();
     67
     68        /**
     69         * Gets the data set that triggered this selection event.
     70         * @return The data set.
     71         */
     72        public DataSet getSource();
     73
     74        /**
     75         * Test if this event did not change anything.
     76         * <p>
     77         * Should return true for all events that are fired.
     78         * @return <code>true</code> if this did not change the selection.
     79         */
     80        default boolean isNop() {
     81            return getAdded().isEmpty() && getRemoved().isEmpty();
     82        }
     83    }
     84
     85    /**
     86     * The base class for selection events
     87     * @author Michael Zangl
     88     * @since xxx
     89     */
     90    abstract static class AbstractSelectionEvent implements SelectionChangeEvent {
     91        private final DataSet source;
     92        private final Set<OsmPrimitive> old;
     93
     94        public AbstractSelectionEvent(DataSet source, Set<OsmPrimitive> old) {
     95            CheckParameterUtil.ensureParameterNotNull(source, "source");
     96            CheckParameterUtil.ensureParameterNotNull(old, "old");
     97            this.source = source;
     98            this.old = Collections.unmodifiableSet(old);
     99        }
     100
     101        @Override
     102        public Set<OsmPrimitive> getOldSelection() {
     103            return old;
     104        }
     105
     106        @Override
     107        public DataSet getSource() {
     108            return source;
     109        }
     110    }
     111
     112
     113    /**
     114     * The selection is replaced by a new selection
     115     * @author Michael Zangl
     116     * @since xxx
     117     */
     118    public static class SelectionReplaceEvent extends AbstractSelectionEvent {
     119        private final Set<OsmPrimitive> current;
     120        private Set<OsmPrimitive> removed;
     121        private Set<OsmPrimitive> added;
     122
     123        /**
     124         * Create a {@link SelectionReplaceEvent}
     125         * @param source The source dataset
     126         * @param old The old primitves that were previously selected. The caller needs to ensure that this set is not modifed.
     127         * @param newSelection The primitives of the new selection.
     128         */
     129        public SelectionReplaceEvent(DataSet source, Set<OsmPrimitive> old, Stream<OsmPrimitive> newSelection) {
     130            super(source, old);
     131            this.current = newSelection.collect(Collectors.toSet());
     132
     133        }
     134
     135        @Override
     136        public Set<OsmPrimitive> getSelection() {
     137            return current;
     138        }
     139
     140        @Override
     141        public synchronized Set<OsmPrimitive> getRemoved() {
     142            if (removed == null) {
     143                removed = getOldSelection().stream().filter(p -> !current.contains(p)).collect(Collectors.toSet());
     144            }
     145            return removed;
     146        }
     147
     148        @Override
     149        public synchronized Set<OsmPrimitive> getAdded() {
     150            if (added == null) {
     151                added = current.stream().filter(p -> !getOldSelection().contains(p)).collect(Collectors.toSet());
     152            }
     153            return added;
     154        }
     155    }
     156
     157    /**
     158     * Primitives are added to the selection
     159     * @author Michael Zangl
     160     * @since xxx
     161     */
     162    public static class SelectionAddEvent extends AbstractSelectionEvent {
     163        private final Set<OsmPrimitive> add;
     164        private final Set<OsmPrimitive> current;
     165
     166        /**
     167         * Create a {@link SelectionAddEvent}
     168         * @param source The source dataset
     169         * @param old The old primitves that were previously selected. The caller needs to ensure that this set is not modifed.
     170         * @param toAdd The primitives to add.
     171         */
     172        public SelectionAddEvent(DataSet source, Set<OsmPrimitive> old, Stream<OsmPrimitive> toAdd) {
     173            super(source, old);
     174            this.add = toAdd.filter(p -> !old.contains(p)).collect(Collectors.toSet());
     175            if (this.add.isEmpty()) {
     176                this.current = this.getOldSelection();
     177            } else {
     178                this.current = new HashSet<>(old);
     179                this.current.addAll(add);
     180            }
     181        }
     182
     183        @Override
     184        public Set<OsmPrimitive> getSelection() {
     185            return current;
     186        }
     187
     188        @Override
     189        public Set<OsmPrimitive> getRemoved() {
     190            return Collections.emptySet();
     191        }
     192
     193        @Override
     194        public Set<OsmPrimitive> getAdded() {
     195            return add;
     196        }
     197    }
     198
     199    /**
     200     * Primitives are removed from the selection
     201     * @author Michael Zangl
     202     * @since xxx
     203     */
     204    public static class SelectionRemoveEvent extends AbstractSelectionEvent {
     205        private final Set<OsmPrimitive> remove;
     206        private final Set<OsmPrimitive> current;
     207
     208        /**
     209         * Create a {@link SelectionRemoveEvent}
     210         * @param source The source dataset
     211         * @param old The old primitves that were previously selected. The caller needs to ensure that this set is not modifed.
     212         * @param toRemove The primitives to remove.
     213         */
     214        public SelectionRemoveEvent(DataSet source, Set<OsmPrimitive> old, Stream<OsmPrimitive> toRemove) {
     215            super(source, old);
     216            this.remove = toRemove.filter(old::contains).collect(Collectors.toSet());
     217            if (this.remove.isEmpty()) {
     218                this.current = this.getOldSelection();
     219            } else {
     220                HashSet<OsmPrimitive> currentSet = new HashSet<>(old);
     221                currentSet.removeAll(remove);
     222                current = Collections.unmodifiableSet(currentSet);
     223            }
     224        }
     225
     226        @Override
     227        public Set<OsmPrimitive> getSelection() {
     228            return current;
     229        }
     230
     231        @Override
     232        public Set<OsmPrimitive> getRemoved() {
     233            return remove;
     234        }
     235
     236        @Override
     237        public Set<OsmPrimitive> getAdded() {
     238            return Collections.emptySet();
     239        }
     240    }
     241
     242    /**
     243     * Toggle the selected state of a primitive
     244     * @author Michael Zangl
     245     * @since xxx
     246     */
     247    public static class SelectionToggleEvent extends AbstractSelectionEvent {
     248        private final Set<OsmPrimitive> current;
     249        private final Set<OsmPrimitive> remove;
     250        private final Set<OsmPrimitive> add;
     251
     252        /**
     253         * Create a {@link SelectionToggleEvent}
     254         * @param source The source dataset
     255         * @param old The old primitves that were previously selected. The caller needs to ensure that this set is not modifed.
     256         * @param toToggle The primitives to toggle.
     257         */
     258        public SelectionToggleEvent(DataSet source, Set<OsmPrimitive> old, Stream<OsmPrimitive> toToggle) {
     259            super(source, old);
     260            HashSet<OsmPrimitive> currentSet = new HashSet<>(old);
     261            HashSet<OsmPrimitive> removeSet = new HashSet<>();
     262            HashSet<OsmPrimitive> addSet = new HashSet<>();
     263            toToggle.forEach(p -> {
     264                if (currentSet.remove(p)) {
     265                    removeSet.add(p);
     266                } else {
     267                    addSet.add(p);
     268                    currentSet.add(p);
     269                }
     270            });
     271            this.current = Collections.unmodifiableSet(currentSet);
     272            this.remove = Collections.unmodifiableSet(removeSet);
     273            this.add = Collections.unmodifiableSet(addSet);
     274        }
     275
     276        @Override
     277        public Set<OsmPrimitive> getSelection() {
     278            return current;
     279        }
     280
     281        @Override
     282        public Set<OsmPrimitive> getRemoved() {
     283            return remove;
     284        }
     285
     286        @Override
     287        public Set<OsmPrimitive> getAdded() {
     288            return add;
     289        }
     290    }
     291}
  • src/org/openstreetmap/josm/data/osm/DataSet.java

    diff --git a/src/org/openstreetmap/josm/data/osm/DataSet.java b/src/org/openstreetmap/josm/data/osm/DataSet.java
    index ae31a3a..5b30af9 100644
    a b import static org.openstreetmap.josm.tools.I18n.tr; 
    55
    66import java.awt.geom.Area;
    77import java.util.ArrayList;
    8 import java.util.Arrays;
    98import java.util.Collection;
    109import java.util.Collections;
    1110import java.util.HashMap;
    1211import java.util.HashSet;
    1312import java.util.Iterator;
    14 import java.util.LinkedHashSet;
    1513import java.util.LinkedList;
    1614import java.util.List;
    1715import java.util.Map;
     16import java.util.Objects;
    1817import java.util.Set;
    1918import java.util.concurrent.CopyOnWriteArrayList;
    2019import java.util.concurrent.locks.Lock;
    2120import java.util.concurrent.locks.ReadWriteLock;
    2221import java.util.concurrent.locks.ReentrantReadWriteLock;
     22import java.util.function.Function;
    2323import java.util.function.Predicate;
     24import java.util.stream.Stream;
    2425
    2526import org.openstreetmap.josm.Main;
    2627import org.openstreetmap.josm.data.Bounds;
    import org.openstreetmap.josm.data.ProjectionBounds; 
    3031import org.openstreetmap.josm.data.SelectionChangedListener;
    3132import org.openstreetmap.josm.data.coor.EastNorth;
    3233import org.openstreetmap.josm.data.coor.LatLon;
     34import org.openstreetmap.josm.data.osm.DataSelectionListener.SelectionAddEvent;
     35import org.openstreetmap.josm.data.osm.DataSelectionListener.SelectionChangeEvent;
     36import org.openstreetmap.josm.data.osm.DataSelectionListener.SelectionRemoveEvent;
     37import org.openstreetmap.josm.data.osm.DataSelectionListener.SelectionReplaceEvent;
     38import org.openstreetmap.josm.data.osm.DataSelectionListener.SelectionToggleEvent;
    3339import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
    3440import org.openstreetmap.josm.data.osm.event.ChangesetIdChangedEvent;
    3541import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
    import org.openstreetmap.josm.data.projection.Projection; 
    4652import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
    4753import org.openstreetmap.josm.gui.progress.ProgressMonitor;
    4854import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
     55import org.openstreetmap.josm.tools.ListenerList;
    4956import org.openstreetmap.josm.tools.SubclassFilteredCollection;
    5057import org.openstreetmap.josm.tools.Utils;
    5158
    public final class DataSet implements Data, Cloneable, ProjectionChangeListener 
    124131
    125132    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    126133    private final Object selectionLock = new Object();
     134    private Set<OsmPrimitive> currentSelectedPrimitives = new HashSet<>();
     135
     136    private final ListenerList<DataSelectionListener> selectionListeners = ListenerList.create();
    127137
    128138    /**
    129139     * Constructs a new {@code DataSet}.
    public final class DataSet implements Data, Cloneable, ProjectionChangeListener 
    132142        // Transparently register as projection change listener. No need to explicitly remove
    133143        // the listener, projection change listeners are managed as WeakReferences.
    134144        Main.addProjectionChangeListener(this);
     145        addSelectionListener((DataSelectionListener) e -> fireDreprecatedSelectionChange(e.getSelection()));
    135146    }
    136147
    137148    /**
    public final class DataSet implements Data, Cloneable, ProjectionChangeListener 
    552563            }
    553564            if (!success)
    554565                throw new RuntimeException("failed to remove primitive: "+primitive);
    555             synchronized (selectionLock) {
    556                 selectedPrimitives.remove(primitive);
    557                 selectionSnapshot = null;
    558             }
     566            clearSelection(primitiveId);
    559567            allPrimitives.remove(primitive);
    560568            primitive.setDataset(null);
    561569            firePrimitivesRemoved(Collections.singletonList(primitive), false);
    public final class DataSet implements Data, Cloneable, ProjectionChangeListener 
    569577     *---------------------------------------------------*/
    570578
    571579    /**
     580     * Add a listener that listens to selection changes in this specific data set.
     581     * @param listener The listener.
     582     * @see #removeSelectionListener(DataSelectionListener)
     583     */
     584    public void addSelectionListener(DataSelectionListener listener) {
     585        selectionListeners.addListener(listener);
     586    }
     587
     588    /**
     589     * Remove a listener that listens to selection changes in this specific data set.
     590     * @param listener The listener.
     591     * @see #addSelectionListener(DataSelectionListener)
     592     */
     593    public void removeSelectionListener(DataSelectionListener listener) {
     594        selectionListeners.removeListener(listener);
     595    }
     596
     597    /*---------------------------------------------------
     598     *   OLD SELECTION HANDLING
     599     *---------------------------------------------------*/
     600
     601    /**
    572602     * A list of listeners to selection changed events. The list is static, as listeners register
    573603     * themselves for any dataset selection changes that occur, regardless of the current active
    574604     * dataset. (However, the selection does only change in the active layer)
    public final class DataSet implements Data, Cloneable, ProjectionChangeListener 
    594624    /**
    595625     * Notifies all registered {@link SelectionChangedListener} about the current selection in
    596626     * this dataset.
    597      *
     627     * @deprecated You should never need to do this from the outside.
    598628     */
     629    @Deprecated
    599630    public void fireSelectionChanged() {
    600         Collection<? extends OsmPrimitive> currentSelection = getAllSelected();
     631        fireDreprecatedSelectionChange(getAllSelected());
     632    }
     633
     634    private static void fireDreprecatedSelectionChange(Collection<? extends OsmPrimitive> currentSelection) {
    601635        for (SelectionChangedListener l : selListeners) {
    602636            l.selectionChanged(currentSelection);
    603637        }
    604638    }
    605639
    606     private Set<OsmPrimitive> selectedPrimitives = new LinkedHashSet<>();
    607     private Collection<OsmPrimitive> selectionSnapshot;
    608 
    609640    /**
    610641     * Returns selected nodes and ways.
    611642     * @return selected nodes and ways
    public final class DataSet implements Data, Cloneable, ProjectionChangeListener 
    651682     * @return unmodifiable collection of primitives
    652683     */
    653684    public Collection<OsmPrimitive> getAllSelected() {
    654         Collection<OsmPrimitive> currentList;
    655         synchronized (selectionLock) {
    656             if (selectionSnapshot == null) {
    657                 selectionSnapshot = Collections.unmodifiableList(new ArrayList<>(selectedPrimitives));
    658             }
    659             currentList = selectionSnapshot;
    660         }
    661         return currentList;
     685        return currentSelectedPrimitives;
    662686    }
    663687
    664688    /**
    public final class DataSet implements Data, Cloneable, ProjectionChangeListener 
    690714     * @return whether the selection is empty or not
    691715     */
    692716    public boolean selectionEmpty() {
    693         return selectedPrimitives.isEmpty();
     717        return currentSelectedPrimitives.isEmpty();
    694718    }
    695719
    696720    /**
    public final class DataSet implements Data, Cloneable, ProjectionChangeListener 
    699723     * @return whether {@code osm} is selected or not
    700724     */
    701725    public boolean isSelected(OsmPrimitive osm) {
    702         return selectedPrimitives.contains(osm);
    703     }
    704 
    705     /**
    706      * Toggles the selected state of the given collection of primitives.
    707      * @param osm The primitives to toggle
    708      */
    709     public void toggleSelected(Collection<? extends PrimitiveId> osm) {
    710         boolean changed = false;
    711         synchronized (selectionLock) {
    712             for (PrimitiveId o : osm) {
    713                 changed = changed | this.dotoggleSelected(o);
    714             }
    715             if (changed) {
    716                 selectionSnapshot = null;
    717             }
    718         }
    719         if (changed) {
    720             fireSelectionChanged();
    721         }
    722     }
    723 
    724     /**
    725      * Toggles the selected state of the given collection of primitives.
    726      * @param osm The primitives to toggle
    727      */
    728     public void toggleSelected(PrimitiveId... osm) {
    729         toggleSelected(Arrays.asList(osm));
    730     }
    731 
    732     private boolean dotoggleSelected(PrimitiveId primitiveId) {
    733         OsmPrimitive primitive = getPrimitiveByIdChecked(primitiveId);
    734         if (primitive == null)
    735             return false;
    736         if (!selectedPrimitives.remove(primitive)) {
    737             selectedPrimitives.add(primitive);
    738         }
    739         selectionSnapshot = null;
    740         return true;
     726        return currentSelectedPrimitives.contains(osm);
    741727    }
    742728
    743729    /**
    public final class DataSet implements Data, Cloneable, ProjectionChangeListener 
    772758     *
    773759     * @param selection the selection
    774760     * @param fireSelectionChangeEvent true, if the selection change listeners are to be notified; false, otherwise
     761     * @deprecated Use {@link #setSelected(Collection)} instead. To bee removed soon.
    775762     */
     763    @Deprecated
    776764    public void setSelected(Collection<? extends PrimitiveId> selection, boolean fireSelectionChangeEvent) {
    777         boolean changed;
    778         synchronized (selectionLock) {
    779             Set<OsmPrimitive> oldSelection = new LinkedHashSet<>(selectedPrimitives);
    780             selectedPrimitives = new LinkedHashSet<>();
    781             addSelected(selection, false);
    782             changed = !oldSelection.equals(selectedPrimitives);
    783             if (changed) {
    784                 selectionSnapshot = null;
    785             }
    786         }
    787 
    788         if (changed && fireSelectionChangeEvent) {
    789             // If selection is not empty then event was already fired in addSelecteds
    790             fireSelectionChanged();
    791         }
     765        setSelected(selection);
    792766    }
    793767
    794768    /**
    public final class DataSet implements Data, Cloneable, ProjectionChangeListener 
    798772     * @param selection the selection
    799773     */
    800774    public void setSelected(Collection<? extends PrimitiveId> selection) {
    801         setSelected(selection, true /* fire selection change event */);
     775        setSelected(selection.stream());
    802776    }
    803777
    804778    /**
    805779     * Sets the current selection to the primitives in <code>osm</code>
    806780     * and notifies all {@link SelectionChangedListener}.
    807781     *
    808      * @param osm the primitives to set
     782     * @param osm the primitives to set. <code>null</code> values are ignored for now, but this may be removed in the future.
    809783     */
    810784    public void setSelected(PrimitiveId... osm) {
    811         if (osm.length == 1 && osm[0] == null) {
    812             setSelected();
    813             return;
    814         }
    815         List<PrimitiveId> list = Arrays.asList(osm);
    816         setSelected(list);
     785        setSelected(Stream.of(osm).filter(Objects::nonNull));
     786    }
     787
     788    private void setSelected(Stream<? extends PrimitiveId> stream) {
     789        doSelectionChange(old -> new SelectionReplaceEvent(this, old,
     790                stream.map(this::getPrimitiveByIdChecked).filter(Objects::nonNull)));
    817791    }
    818792
    819793    /**
    public final class DataSet implements Data, Cloneable, ProjectionChangeListener 
    823797     * @param selection the selection
    824798     */
    825799    public void addSelected(Collection<? extends PrimitiveId> selection) {
    826         addSelected(selection, true /* fire selection change event */);
     800        addSelected(selection.stream());
    827801    }
    828802
    829803    /**
    public final class DataSet implements Data, Cloneable, ProjectionChangeListener 
    833807     * @param osm the primitives to add
    834808     */
    835809    public void addSelected(PrimitiveId... osm) {
    836         addSelected(Arrays.asList(osm));
     810        addSelected(Stream.of(osm));
     811    }
     812
     813    private void addSelected(Stream<? extends PrimitiveId> stream) {
     814        doSelectionChange(old -> new SelectionAddEvent(this, old,
     815                stream.map(this::getPrimitiveByIdChecked).filter(Objects::nonNull)));
    837816    }
    838817
    839818    /**
    840      * Adds the primitives in <code>selection</code> to the current selection.
    841      * Notifies all {@link SelectionChangedListener} if <code>fireSelectionChangeEvent</code> is true.
    842      *
    843      * @param selection the selection
    844      * @param fireSelectionChangeEvent true, if the selection change listeners are to be notified; false, otherwise
    845      * @return if the selection was changed in the process
    846      */
    847     private boolean addSelected(Collection<? extends PrimitiveId> selection, boolean fireSelectionChangeEvent) {
    848         boolean changed = false;
    849         synchronized (selectionLock) {
    850             for (PrimitiveId id: selection) {
    851                 OsmPrimitive primitive = getPrimitiveByIdChecked(id);
    852                 if (primitive != null) {
    853                     changed = changed | selectedPrimitives.add(primitive);
    854                 }
    855             }
    856             if (changed) {
    857                 selectionSnapshot = null;
    858             }
    859         }
    860         if (fireSelectionChangeEvent && changed) {
    861             fireSelectionChanged();
    862         }
    863         return changed;
     819     * Removes the selection from every value in the collection.
     820     * @param osm The collection of ids to remove the selection from.
     821     */
     822    public void clearSelection(PrimitiveId... osm) {
     823        clearSelection(Stream.of(osm));
    864824    }
    865825
    866826    /**
    867      * clear all highlights of virtual nodes
     827     * Removes the selection from every value in the collection.
     828     * @param list The collection of ids to remove the selection from.
    868829     */
    869     public void clearHighlightedVirtualNodes() {
    870         setHighlightedVirtualNodes(new ArrayList<WaySegment>());
     830    public void clearSelection(Collection<? extends PrimitiveId> list) {
     831        clearSelection(list.stream());
    871832    }
    872833
    873834    /**
    874      * clear all highlights of way segments
     835     * Clears the current selection.
    875836     */
    876     public void clearHighlightedWaySegments() {
    877         setHighlightedWaySegments(new ArrayList<WaySegment>());
     837    public void clearSelection() {
     838        setSelected(Stream.empty());
     839    }
     840
     841    private void clearSelection(Stream<? extends PrimitiveId> stream) {
     842        doSelectionChange(old -> new SelectionRemoveEvent(this, old,
     843                stream.map(this::getPrimitiveByIdChecked).filter(Objects::nonNull)));
    878844    }
    879845
    880846    /**
    881      * Removes the selection from every value in the collection.
    882      * @param osm The collection of ids to remove the selection from.
     847     * Toggles the selected state of the given collection of primitives.
     848     * @param osm The primitives to toggle
    883849     */
    884     public void clearSelection(PrimitiveId... osm) {
    885         clearSelection(Arrays.asList(osm));
     850    public void toggleSelected(Collection<? extends PrimitiveId> osm) {
     851        toggleSelected(osm.stream());
    886852    }
    887853
    888854    /**
    889      * Removes the selection from every value in the collection.
    890      * @param list The collection of ids to remove the selection from.
     855     * Toggles the selected state of the given collection of primitives.
     856     * @param osm The primitives to toggle
    891857     */
    892     public void clearSelection(Collection<? extends PrimitiveId> list) {
    893         boolean changed = false;
    894         synchronized (selectionLock) {
    895             for (PrimitiveId id:list) {
    896                 OsmPrimitive primitive = getPrimitiveById(id);
    897                 if (primitive != null) {
    898                     changed = changed | selectedPrimitives.remove(primitive);
    899                 }
    900             }
    901             if (changed) {
    902                 selectionSnapshot = null;
    903             }
    904         }
    905         if (changed) {
    906             fireSelectionChanged();
    907         }
     858    public void toggleSelected(PrimitiveId... osm) {
     859        toggleSelected(Stream.of(osm));
     860    }
     861
     862    private void toggleSelected(Stream<? extends PrimitiveId> stream) {
     863        doSelectionChange(old -> new SelectionToggleEvent(this, old,
     864                stream.map(this::getPrimitiveByIdChecked).filter(Objects::nonNull)));
    908865    }
    909866
    910867    /**
    911      * Clears the current selection.
     868     * Do a selection change.
     869     * <p>
     870     * This is the only method that changes the current selection state.
     871     * @param command A generator that generates the {@link SelectionChangeEvent} for the given base set of currently selected primitives.
     872     * @return if the command did change the selection.
    912873     */
    913     public void clearSelection() {
    914         if (!selectedPrimitives.isEmpty()) {
     874    private boolean doSelectionChange(Function<Set<OsmPrimitive>, SelectionChangeEvent> command) {
     875        lock.readLock().lock();
     876        try {
    915877            synchronized (selectionLock) {
    916                 selectedPrimitives.clear();
    917                 selectionSnapshot = null;
     878                SelectionChangeEvent event = command.apply(currentSelectedPrimitives);
     879                if (event.isNop()) {
     880                    return false;
     881                }
     882                currentSelectedPrimitives = event.getSelection();
     883                selectionListeners.fireEvent(l -> l.selectionChanged(event));
     884                return true;
    918885            }
    919             fireSelectionChanged();
     886        } finally {
     887            lock.readLock().unlock();
    920888        }
    921889    }
    922890
    923891    /**
     892     * clear all highlights of virtual nodes
     893     */
     894    public void clearHighlightedVirtualNodes() {
     895        setHighlightedVirtualNodes(new ArrayList<WaySegment>());
     896    }
     897
     898    /**
     899     * clear all highlights of way segments
     900     */
     901    public void clearHighlightedWaySegments() {
     902        setHighlightedWaySegments(new ArrayList<WaySegment>());
     903    }
     904
     905    /**
    924906     * Return a copy of this dataset
    925907     * @deprecated Use the copy constructor instead. Remove in July 2016
    926908     */
    public final class DataSet implements Data, Cloneable, ProjectionChangeListener 
    12671249    public void cleanupDeletedPrimitives() {
    12681250        beginUpdate();
    12691251        try {
    1270             boolean changed = cleanupDeleted(nodes.iterator());
    1271             if (cleanupDeleted(ways.iterator())) {
    1272                 changed = true;
    1273             }
    1274             if (cleanupDeleted(relations.iterator())) {
    1275                 changed = true;
    1276             }
    1277             if (changed) {
    1278                 fireSelectionChanged();
    1279             }
     1252            cleanupDeleted(Stream.concat(
     1253                    nodes.stream(), Stream.concat(ways.stream(), relations.stream())));
    12801254        } finally {
    12811255            endUpdate();
    12821256        }
    12831257    }
    12841258
    1285     private boolean cleanupDeleted(Iterator<? extends OsmPrimitive> it) {
    1286         boolean changed = false;
    1287         synchronized (selectionLock) {
    1288             while (it.hasNext()) {
    1289                 OsmPrimitive primitive = it.next();
    1290                 if (primitive.isDeleted() && (!primitive.isVisible() || primitive.isNew())) {
    1291                     selectedPrimitives.remove(primitive);
    1292                     selectionSnapshot = null;
    1293                     allPrimitives.remove(primitive);
    1294                     primitive.setDataset(null);
    1295                     changed = true;
    1296                     it.remove();
    1297                 }
    1298             }
    1299             if (changed) {
    1300                 selectionSnapshot = null;
    1301             }
    1302         }
    1303         return changed;
     1259    private void cleanupDeleted(Stream<? extends OsmPrimitive> it) {
     1260        clearSelection(it
     1261                .filter(primitive -> primitive.isDeleted() && (!primitive.isVisible() || primitive.isNew()))
     1262                .peek(allPrimitives::remove)
     1263                .peek(primitive -> primitive.setDataset(null)));
    13041264    }
    13051265
    13061266    /**