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    /**
118     * The selection is replaced by a new selection
119     * @author Michael Zangl
120     * @since 12048
121     */
122    class SelectionReplaceEvent extends AbstractSelectionEvent {
123        private final Set<OsmPrimitive> current;
124        private Set<OsmPrimitive> removed;
125        private Set<OsmPrimitive> added;
126
127        /**
128         * Create a {@link SelectionReplaceEvent}
129         * @param source The source dataset
130         * @param old The old primitves that were previously selected. The caller needs to ensure that this set is not modifed.
131         * @param newSelection The primitives of the new selection.
132         */
133        public SelectionReplaceEvent(DataSet source, Set<OsmPrimitive> old, Stream<OsmPrimitive> newSelection) {
134            super(source, old);
135            this.current = newSelection.collect(Collectors.toCollection(LinkedHashSet::new));
136        }
137
138        @Override
139        public Set<OsmPrimitive> getSelection() {
140            return current;
141        }
142
143        @Override
144        public synchronized Set<OsmPrimitive> getRemoved() {
145            if (removed == null) {
146                removed = getOldSelection().stream()
147                        .filter(p -> !current.contains(p))
148                        .collect(Collectors.toCollection(LinkedHashSet::new));
149            }
150            return removed;
151        }
152
153        @Override
154        public synchronized Set<OsmPrimitive> getAdded() {
155            if (added == null) {
156                added = current.stream()
157                        .filter(p -> !getOldSelection().contains(p)).collect(Collectors.toCollection(LinkedHashSet::new));
158            }
159            return added;
160        }
161
162        @Override
163        public String toString() {
164            return "SelectionReplaceEvent [current=" + current + ", removed=" + removed + ", added=" + added + ']';
165        }
166    }
167
168    /**
169     * Primitives are added to the selection
170     * @author Michael Zangl
171     * @since 12048
172     */
173    class SelectionAddEvent extends AbstractSelectionEvent {
174        private final Set<OsmPrimitive> add;
175        private final Set<OsmPrimitive> current;
176
177        /**
178         * Create a {@link SelectionAddEvent}
179         * @param source The source dataset
180         * @param old The old primitves that were previously selected. The caller needs to ensure that this set is not modifed.
181         * @param toAdd The primitives to add.
182         */
183        public SelectionAddEvent(DataSet source, Set<OsmPrimitive> old, Stream<OsmPrimitive> toAdd) {
184            super(source, old);
185            this.add = toAdd
186                    .filter(p -> !old.contains(p))
187                    .collect(Collectors.toCollection(LinkedHashSet::new));
188            if (this.add.isEmpty()) {
189                this.current = this.getOldSelection();
190            } else {
191                this.current = new LinkedHashSet<>(old);
192                this.current.addAll(add);
193            }
194        }
195
196        @Override
197        public Set<OsmPrimitive> getSelection() {
198            return Collections.unmodifiableSet(current);
199        }
200
201        @Override
202        public Set<OsmPrimitive> getRemoved() {
203            return Collections.emptySet();
204        }
205
206        @Override
207        public Set<OsmPrimitive> getAdded() {
208            return Collections.unmodifiableSet(add);
209        }
210
211        @Override
212        public String toString() {
213            return "SelectionAddEvent [add=" + add + ", current=" + current + ']';
214        }
215    }
216
217    /**
218     * Primitives are removed from the selection
219     * @author Michael Zangl
220     * @since 12048
221     */
222    class SelectionRemoveEvent extends AbstractSelectionEvent {
223        private final Set<OsmPrimitive> remove;
224        private final Set<OsmPrimitive> current;
225
226        /**
227         * Create a {@link SelectionRemoveEvent}
228         * @param source The source dataset
229         * @param old The old primitves that were previously selected. The caller needs to ensure that this set is not modifed.
230         * @param toRemove The primitives to remove.
231         */
232        public SelectionRemoveEvent(DataSet source, Set<OsmPrimitive> old, Stream<OsmPrimitive> toRemove) {
233            super(source, old);
234            this.remove = toRemove
235                    .filter(old::contains)
236                    .collect(Collectors.toCollection(LinkedHashSet::new));
237            if (this.remove.isEmpty()) {
238                this.current = this.getOldSelection();
239            } else {
240                HashSet<OsmPrimitive> currentSet = new LinkedHashSet<>(old);
241                currentSet.removeAll(remove);
242                current = currentSet;
243            }
244        }
245
246        @Override
247        public Set<OsmPrimitive> getSelection() {
248            return Collections.unmodifiableSet(current);
249        }
250
251        @Override
252        public Set<OsmPrimitive> getRemoved() {
253            return Collections.unmodifiableSet(remove);
254        }
255
256        @Override
257        public Set<OsmPrimitive> getAdded() {
258            return Collections.emptySet();
259        }
260
261        @Override
262        public String toString() {
263            return "SelectionRemoveEvent [remove=" + remove + ", current=" + current + ']';
264        }
265    }
266
267    /**
268     * Toggle the selected state of a primitive
269     * @author Michael Zangl
270     * @since 12048
271     */
272    class SelectionToggleEvent extends AbstractSelectionEvent {
273        private final Set<OsmPrimitive> current;
274        private final Set<OsmPrimitive> remove;
275        private final Set<OsmPrimitive> add;
276
277        /**
278         * Create a {@link SelectionToggleEvent}
279         * @param source The source dataset
280         * @param old The old primitves that were previously selected. The caller needs to ensure that this set is not modifed.
281         * @param toToggle The primitives to toggle.
282         */
283        public SelectionToggleEvent(DataSet source, Set<OsmPrimitive> old, Stream<OsmPrimitive> toToggle) {
284            super(source, old);
285            HashSet<OsmPrimitive> currentSet = new LinkedHashSet<>(old);
286            HashSet<OsmPrimitive> removeSet = new LinkedHashSet<>();
287            HashSet<OsmPrimitive> addSet = new LinkedHashSet<>();
288            toToggle.forEach(p -> {
289                if (currentSet.remove(p)) {
290                    removeSet.add(p);
291                } else {
292                    addSet.add(p);
293                    currentSet.add(p);
294                }
295            });
296            this.current = Collections.unmodifiableSet(currentSet);
297            this.remove = Collections.unmodifiableSet(removeSet);
298            this.add = Collections.unmodifiableSet(addSet);
299        }
300
301        @Override
302        public Set<OsmPrimitive> getSelection() {
303            return Collections.unmodifiableSet(current);
304        }
305
306        @Override
307        public Set<OsmPrimitive> getRemoved() {
308            return Collections.unmodifiableSet(remove);
309        }
310
311        @Override
312        public Set<OsmPrimitive> getAdded() {
313            return Collections.unmodifiableSet(add);
314        }
315
316        @Override
317        public String toString() {
318            return "SelectionToggleEvent [current=" + current + ", remove=" + remove + ", add=" + add + ']';
319        }
320    }
321}