001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm;
003
004import java.util.Collections;
005import java.util.HashSet;
006import java.util.LinkedHashSet;
007import java.util.Set;
008import java.util.stream.Collectors;
009import java.util.stream.Stream;
010
011import org.openstreetmap.josm.tools.CheckParameterUtil;
012
013/**
014 * This is a listener that listens to selection change events in the data set.
015 * @author Michael Zangl
016 * @since 12048
017 */
018@FunctionalInterface
019public interface DataSelectionListener {
020
021    /**
022     * Called whenever the selection is changed.
023     *
024     * You get notified about the new selection, the elements that were added and removed and the layer that triggered the event.
025     * @param event The selection change event.
026     * @see SelectionChangeEvent
027     */
028    void selectionChanged(SelectionChangeEvent event);
029
030    /**
031     * The event that is fired when the selection changed.
032     * @author Michael Zangl
033     * @since 12048
034     */
035    interface SelectionChangeEvent {
036        /**
037         * Gets the previous selection
038         * <p>
039         * This collection cannot be modified and will not change.
040         * @return The old selection
041         */
042        Set<OsmPrimitive> getOldSelection();
043
044        /**
045         * Gets the new selection. New elements are added to the end of the collection.
046         * <p>
047         * This collection cannot be modified and will not change.
048         * @return The new selection
049         */
050        Set<OsmPrimitive> getSelection();
051
052        /**
053         * Gets the primitives that have been removed from the selection.
054         * <p>
055         * Those are the primitives contained in {@link #getOldSelection()} but not in {@link #getSelection()}
056         * <p>
057         * This collection cannot be modified and will not change.
058         * @return The primitives that were removed
059         */
060        Set<OsmPrimitive> getRemoved();
061
062        /**
063         * Gets the primitives that have been added to the selection.
064         * <p>
065         * Those are the primitives contained in {@link #getSelection()} but not in {@link #getOldSelection()}
066         * <p>
067         * This collection cannot be modified and will not change.
068         * @return The primitives that were added
069         */
070        Set<OsmPrimitive> getAdded();
071
072        /**
073         * Gets the data set that triggered this selection event.
074         * @return The data set.
075         */
076        DataSet getSource();
077
078        /**
079         * Test if this event did not change anything.
080         * <p>
081         * This will return <code>false</code> for all events that are sent to listeners, so you don't need to test it.
082         * @return <code>true</code> if this did not change the selection.
083         */
084        default boolean isNop() {
085            return getAdded().isEmpty() && getRemoved().isEmpty();
086        }
087    }
088
089    /**
090     * The base class for selection events
091     * @author Michael Zangl
092     * @since 12048
093     */
094    abstract class AbstractSelectionEvent implements SelectionChangeEvent {
095        private final DataSet source;
096        private final Set<OsmPrimitive> old;
097
098        public AbstractSelectionEvent(DataSet source, Set<OsmPrimitive> old) {
099            CheckParameterUtil.ensureParameterNotNull(source, "source");
100            CheckParameterUtil.ensureParameterNotNull(old, "old");
101            this.source = source;
102            this.old = Collections.unmodifiableSet(old);
103        }
104
105        @Override
106        public Set<OsmPrimitive> getOldSelection() {
107            return old;
108        }
109
110        @Override
111        public DataSet getSource() {
112            return source;
113        }
114    }
115
116    /**
117     * The selection is replaced by a new selection
118     * @author Michael Zangl
119     * @since 12048
120     */
121    class SelectionReplaceEvent extends AbstractSelectionEvent {
122        private final Set<OsmPrimitive> current;
123        private Set<OsmPrimitive> removed;
124        private Set<OsmPrimitive> added;
125
126        /**
127         * Create a {@link SelectionReplaceEvent}
128         * @param source The source dataset
129         * @param old The old primitves that were previously selected. The caller needs to ensure that this set is not modifed.
130         * @param newSelection The primitives of the new selection.
131         */
132        public SelectionReplaceEvent(DataSet source, Set<OsmPrimitive> old, Stream<OsmPrimitive> newSelection) {
133            super(source, old);
134            this.current = newSelection.collect(Collectors.toCollection(LinkedHashSet::new));
135        }
136
137        @Override
138        public Set<OsmPrimitive> getSelection() {
139            return current;
140        }
141
142        @Override
143        public synchronized Set<OsmPrimitive> getRemoved() {
144            if (removed == null) {
145                removed = getOldSelection().stream()
146                        .filter(p -> !current.contains(p))
147                        .collect(Collectors.toCollection(LinkedHashSet::new));
148            }
149            return removed;
150        }
151
152        @Override
153        public synchronized Set<OsmPrimitive> getAdded() {
154            if (added == null) {
155                added = current.stream()
156                        .filter(p -> !getOldSelection().contains(p)).collect(Collectors.toCollection(LinkedHashSet::new));
157            }
158            return added;
159        }
160
161        @Override
162        public String toString() {
163            return "SelectionReplaceEvent [current=" + current + ", removed=" + removed + ", added=" + added + ']';
164        }
165    }
166
167    /**
168     * Primitives are added to the selection
169     * @author Michael Zangl
170     * @since 12048
171     */
172    class SelectionAddEvent extends AbstractSelectionEvent {
173        private final Set<OsmPrimitive> add;
174        private final Set<OsmPrimitive> current;
175
176        /**
177         * Create a {@link SelectionAddEvent}
178         * @param source The source dataset
179         * @param old The old primitves that were previously selected. The caller needs to ensure that this set is not modifed.
180         * @param toAdd The primitives to add.
181         */
182        public SelectionAddEvent(DataSet source, Set<OsmPrimitive> old, Stream<OsmPrimitive> toAdd) {
183            super(source, old);
184            this.add = toAdd
185                    .filter(p -> !old.contains(p))
186                    .collect(Collectors.toCollection(LinkedHashSet::new));
187            if (this.add.isEmpty()) {
188                this.current = this.getOldSelection();
189            } else {
190                this.current = new LinkedHashSet<>(old);
191                this.current.addAll(add);
192            }
193        }
194
195        @Override
196        public Set<OsmPrimitive> getSelection() {
197            return Collections.unmodifiableSet(current);
198        }
199
200        @Override
201        public Set<OsmPrimitive> getRemoved() {
202            return Collections.emptySet();
203        }
204
205        @Override
206        public Set<OsmPrimitive> getAdded() {
207            return Collections.unmodifiableSet(add);
208        }
209
210        @Override
211        public String toString() {
212            return "SelectionAddEvent [add=" + add + ", current=" + current + ']';
213        }
214    }
215
216    /**
217     * Primitives are removed from the selection
218     * @author Michael Zangl
219     * @since 12048
220     */
221    class SelectionRemoveEvent extends AbstractSelectionEvent {
222        private final Set<OsmPrimitive> remove;
223        private final Set<OsmPrimitive> current;
224
225        /**
226         * Create a {@link SelectionRemoveEvent}
227         * @param source The source dataset
228         * @param old The old primitves that were previously selected. The caller needs to ensure that this set is not modifed.
229         * @param toRemove The primitives to remove.
230         */
231        public SelectionRemoveEvent(DataSet source, Set<OsmPrimitive> old, Stream<OsmPrimitive> toRemove) {
232            super(source, old);
233            this.remove = toRemove
234                    .filter(old::contains)
235                    .collect(Collectors.toCollection(LinkedHashSet::new));
236            if (this.remove.isEmpty()) {
237                this.current = this.getOldSelection();
238            } else {
239                HashSet<OsmPrimitive> currentSet = new LinkedHashSet<>(old);
240                currentSet.removeAll(remove);
241                current = currentSet;
242            }
243        }
244
245        @Override
246        public Set<OsmPrimitive> getSelection() {
247            return Collections.unmodifiableSet(current);
248        }
249
250        @Override
251        public Set<OsmPrimitive> getRemoved() {
252            return Collections.unmodifiableSet(remove);
253        }
254
255        @Override
256        public Set<OsmPrimitive> getAdded() {
257            return Collections.emptySet();
258        }
259
260        @Override
261        public String toString() {
262            return "SelectionRemoveEvent [remove=" + remove + ", current=" + current + ']';
263        }
264    }
265
266    /**
267     * Toggle the selected state of a primitive
268     * @author Michael Zangl
269     * @since 12048
270     */
271    class SelectionToggleEvent extends AbstractSelectionEvent {
272        private final Set<OsmPrimitive> current;
273        private final Set<OsmPrimitive> remove;
274        private final Set<OsmPrimitive> add;
275
276        /**
277         * Create a {@link SelectionToggleEvent}
278         * @param source The source dataset
279         * @param old The old primitves that were previously selected. The caller needs to ensure that this set is not modifed.
280         * @param toToggle The primitives to toggle.
281         */
282        public SelectionToggleEvent(DataSet source, Set<OsmPrimitive> old, Stream<OsmPrimitive> toToggle) {
283            super(source, old);
284            HashSet<OsmPrimitive> currentSet = new LinkedHashSet<>(old);
285            HashSet<OsmPrimitive> removeSet = new LinkedHashSet<>();
286            HashSet<OsmPrimitive> addSet = new LinkedHashSet<>();
287            toToggle.forEach(p -> {
288                if (currentSet.remove(p)) {
289                    removeSet.add(p);
290                } else {
291                    addSet.add(p);
292                    currentSet.add(p);
293                }
294            });
295            this.current = Collections.unmodifiableSet(currentSet);
296            this.remove = Collections.unmodifiableSet(removeSet);
297            this.add = Collections.unmodifiableSet(addSet);
298        }
299
300        @Override
301        public Set<OsmPrimitive> getSelection() {
302            return Collections.unmodifiableSet(current);
303        }
304
305        @Override
306        public Set<OsmPrimitive> getRemoved() {
307            return Collections.unmodifiableSet(remove);
308        }
309
310        @Override
311        public Set<OsmPrimitive> getAdded() {
312            return Collections.unmodifiableSet(add);
313        }
314
315        @Override
316        public String toString() {
317            return "SelectionToggleEvent [current=" + current + ", remove=" + remove + ", add=" + add + ']';
318        }
319    }
320}