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