001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm.event;
003
004import java.util.ArrayList;
005import java.util.Arrays;
006import java.util.List;
007import java.util.Objects;
008import java.util.Queue;
009import java.util.concurrent.CopyOnWriteArrayList;
010import java.util.concurrent.LinkedBlockingQueue;
011
012import javax.swing.SwingUtilities;
013
014import org.openstreetmap.josm.data.osm.DataSet;
015import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener;
016import org.openstreetmap.josm.gui.MainApplication;
017import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
018import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
019import org.openstreetmap.josm.tools.Logging;
020
021/**
022 * This class allows to add DatasetListener to currently active dataset. If active
023 * layer is changed, listeners are automatically registered at new active dataset
024 * (it's no longer necessary to register for layer events and reregister every time
025 * new layer is selected)
026 *
027 * Events in EDT are supported, see {@link #addDatasetListener(DataSetListener, FireMode)}
028 *
029 */
030public class DatasetEventManager implements ActiveLayerChangeListener, Listener {
031
032    private static final DatasetEventManager INSTANCE = new DatasetEventManager();
033
034    private final class EdtRunnable implements Runnable {
035        @Override
036        public void run() {
037            while (!eventsInEDT.isEmpty()) {
038                DataSet dataSet = null;
039                AbstractDatasetChangedEvent consolidatedEvent = null;
040                AbstractDatasetChangedEvent event;
041
042                while ((event = eventsInEDT.poll()) != null) {
043                    fireEvents(inEDTListeners, event);
044
045                    // DataSet changed - fire consolidated event early
046                    if (consolidatedEvent != null && dataSet != event.getDataset()) {
047                        fireConsolidatedEvents(inEDTListeners, consolidatedEvent);
048                        consolidatedEvent = null;
049                    }
050
051                    dataSet = event.getDataset();
052
053                    // Build consolidated event
054                    if (event instanceof DataChangedEvent) {
055                        // DataChangeEvent can contains other events, so it gets special handling
056                        DataChangedEvent dataEvent = (DataChangedEvent) event;
057                        if (dataEvent.getEvents() == null) {
058                            consolidatedEvent = dataEvent; // Dataset was completely changed, we can ignore older events
059                        } else {
060                            if (consolidatedEvent == null) {
061                                consolidatedEvent = new DataChangedEvent(dataSet, dataEvent.getEvents());
062                            } else if (consolidatedEvent instanceof DataChangedEvent) {
063                                List<AbstractDatasetChangedEvent> evts = ((DataChangedEvent) consolidatedEvent).getEvents();
064                                if (evts != null) {
065                                    evts.addAll(dataEvent.getEvents());
066                                }
067                            } else {
068                                AbstractDatasetChangedEvent oldConsolidateEvent = consolidatedEvent;
069                                consolidatedEvent = new DataChangedEvent(dataSet, dataEvent.getEvents());
070                                ((DataChangedEvent) consolidatedEvent).getEvents().add(oldConsolidateEvent);
071                            }
072                        }
073                    } else {
074                        // Normal events
075                        if (consolidatedEvent == null) {
076                            consolidatedEvent = event;
077                        } else if (consolidatedEvent instanceof DataChangedEvent) {
078                            List<AbstractDatasetChangedEvent> evs = ((DataChangedEvent) consolidatedEvent).getEvents();
079                            if (evs != null) {
080                                evs.add(event);
081                            }
082                        } else {
083                            consolidatedEvent = new DataChangedEvent(dataSet, new ArrayList<>(Arrays.asList(consolidatedEvent)));
084                        }
085                    }
086                }
087
088                // Fire consolidated event
089                if (consolidatedEvent != null) {
090                    fireConsolidatedEvents(inEDTListeners, consolidatedEvent);
091                }
092            }
093        }
094    }
095
096    /**
097     * Event firing mode regarding Event Dispatch Thread.
098     */
099    public enum FireMode {
100        /**
101         * Fire in calling thread immediately.
102         */
103        IMMEDIATELY,
104        /**
105         * Fire in event dispatch thread.
106         */
107        IN_EDT,
108        /**
109         * Fire in event dispatch thread. If more than one event arrived when event queue is checked, merged them to one event
110         */
111        IN_EDT_CONSOLIDATED
112    }
113
114    private static class ListenerInfo {
115        private final DataSetListener listener;
116        private final boolean consolidate;
117
118        ListenerInfo(DataSetListener listener, boolean consolidate) {
119            this.listener = listener;
120            this.consolidate = consolidate;
121        }
122
123        @Override
124        public int hashCode() {
125            return Objects.hash(listener);
126        }
127
128        @Override
129        public boolean equals(Object o) {
130            if (this == o) return true;
131            if (o == null || getClass() != o.getClass()) return false;
132            ListenerInfo that = (ListenerInfo) o;
133            return Objects.equals(listener, that.listener);
134        }
135    }
136
137    /**
138     * Replies the unique instance.
139     * @return the unique instance
140     */
141    public static DatasetEventManager getInstance() {
142        return INSTANCE;
143    }
144
145    private final Queue<AbstractDatasetChangedEvent> eventsInEDT = new LinkedBlockingQueue<>();
146    private final CopyOnWriteArrayList<ListenerInfo> inEDTListeners = new CopyOnWriteArrayList<>();
147    private final CopyOnWriteArrayList<ListenerInfo> normalListeners = new CopyOnWriteArrayList<>();
148    private final DataSetListener myListener = new DataSetListenerAdapter(this);
149    private final Runnable edtRunnable = new EdtRunnable();
150
151    /**
152     * Constructs a new {@code DatasetEventManager}.
153     */
154    public DatasetEventManager() {
155        MainApplication.getLayerManager().addActiveLayerChangeListener(this);
156    }
157
158    /**
159     * Register listener, that will receive events from currently active dataset
160     * @param listener the listener to be registered
161     * @param fireMode If {@link FireMode#IN_EDT} or {@link FireMode#IN_EDT_CONSOLIDATED},
162     * listener will be notified in event dispatch thread instead of thread that caused
163     * the dataset change
164     */
165    public void addDatasetListener(DataSetListener listener, FireMode fireMode) {
166        if (fireMode == FireMode.IN_EDT || fireMode == FireMode.IN_EDT_CONSOLIDATED) {
167            inEDTListeners.addIfAbsent(new ListenerInfo(listener, fireMode == FireMode.IN_EDT_CONSOLIDATED));
168        } else {
169            normalListeners.addIfAbsent(new ListenerInfo(listener, false));
170        }
171    }
172
173    /**
174     * Unregister listener.
175     * @param listener listener to remove
176     */
177    public void removeDatasetListener(DataSetListener listener) {
178        ListenerInfo searchListener = new ListenerInfo(listener, false);
179        inEDTListeners.remove(searchListener);
180        normalListeners.remove(searchListener);
181    }
182
183    @Override
184    public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
185        DataSet oldData = e.getPreviousDataSet();
186        if (oldData != null) {
187            oldData.removeDataSetListener(myListener);
188        }
189
190        DataSet newData = e.getSource().getActiveDataSet();
191        if (newData != null) {
192            newData.addDataSetListener(myListener);
193        }
194        processDatasetEvent(new DataChangedEvent(newData));
195    }
196
197    private static void fireEvents(List<ListenerInfo> listeners, AbstractDatasetChangedEvent event) {
198        for (ListenerInfo listener: listeners) {
199            if (!listener.consolidate) {
200                Logging.trace("Firing {0} to {1} (normal)", event, listener.listener);
201                event.fire(listener.listener);
202            }
203        }
204    }
205
206    private static void fireConsolidatedEvents(List<ListenerInfo> listeners, AbstractDatasetChangedEvent event) {
207        for (ListenerInfo listener: listeners) {
208            if (listener.consolidate) {
209                Logging.trace("Firing {0} to {1} (consolidated)", event, listener.listener);
210                event.fire(listener.listener);
211            }
212        }
213    }
214
215    @Override
216    public void processDatasetEvent(AbstractDatasetChangedEvent event) {
217        fireEvents(normalListeners, event);
218        eventsInEDT.add(event);
219        SwingUtilities.invokeLater(edtRunnable);
220    }
221}