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.Main;
015import org.openstreetmap.josm.data.osm.DataSet;
016import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener;
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                fireConsolidatedEvents(inEDTListeners, consolidatedEvent);
089            }
090        }
091    }
092
093    public enum FireMode {
094        /**
095         * Fire in calling thread immediately.
096         */
097        IMMEDIATELY,
098        /**
099         * Fire in event dispatch thread.
100         */
101        IN_EDT,
102        /**
103         * Fire in event dispatch thread. If more than one event arrived when event queue is checked, merged them to one event
104         */
105        IN_EDT_CONSOLIDATED
106    }
107
108    private static class ListenerInfo {
109        private final DataSetListener listener;
110        private final boolean consolidate;
111
112        ListenerInfo(DataSetListener listener, boolean consolidate) {
113            this.listener = listener;
114            this.consolidate = consolidate;
115        }
116
117        @Override
118        public int hashCode() {
119            return Objects.hash(listener);
120        }
121
122        @Override
123        public boolean equals(Object o) {
124            if (this == o) return true;
125            if (o == null || getClass() != o.getClass()) return false;
126            ListenerInfo that = (ListenerInfo) o;
127            return Objects.equals(listener, that.listener);
128        }
129    }
130
131    /**
132     * Replies the unique instance.
133     * @return the unique instance
134     */
135    public static DatasetEventManager getInstance() {
136        return instance;
137    }
138
139    private final Queue<AbstractDatasetChangedEvent> eventsInEDT = new LinkedBlockingQueue<>();
140    private final CopyOnWriteArrayList<ListenerInfo> inEDTListeners = new CopyOnWriteArrayList<>();
141    private final CopyOnWriteArrayList<ListenerInfo> normalListeners = new CopyOnWriteArrayList<>();
142    private final DataSetListener myListener = new DataSetListenerAdapter(this);
143    private final Runnable edtRunnable = new EdtRunnable();
144
145    /**
146     * Constructs a new {@code DatasetEventManager}.
147     */
148    public DatasetEventManager() {
149        Main.getLayerManager().addActiveLayerChangeListener(this);
150    }
151
152    /**
153     * Register listener, that will receive events from currently active dataset
154     * @param listener the listener to be registered
155     * @param fireMode If {@link FireMode#IN_EDT} or {@link FireMode#IN_EDT_CONSOLIDATED},
156     * listener will be notified in event dispatch thread instead of thread that caused
157     * the dataset change
158     */
159    public void addDatasetListener(DataSetListener listener, FireMode fireMode) {
160        if (fireMode == FireMode.IN_EDT || fireMode == FireMode.IN_EDT_CONSOLIDATED) {
161            inEDTListeners.addIfAbsent(new ListenerInfo(listener, fireMode == FireMode.IN_EDT_CONSOLIDATED));
162        } else {
163            normalListeners.addIfAbsent(new ListenerInfo(listener, false));
164        }
165    }
166
167    public void removeDatasetListener(DataSetListener listener) {
168        ListenerInfo searchListener = new ListenerInfo(listener, false);
169        inEDTListeners.remove(searchListener);
170        normalListeners.remove(searchListener);
171    }
172
173    @Override
174    public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
175        DataSet oldData = e.getPreviousEditDataSet();
176        if (oldData != null) {
177            oldData.removeDataSetListener(myListener);
178        }
179
180        DataSet newData = e.getSource().getEditDataSet();
181        if (newData != null) {
182            newData.addDataSetListener(myListener);
183        }
184        processDatasetEvent(new DataChangedEvent(newData));
185    }
186
187    private static void fireEvents(List<ListenerInfo> listeners, AbstractDatasetChangedEvent event) {
188        for (ListenerInfo listener: listeners) {
189            if (!listener.consolidate) {
190                event.fire(listener.listener);
191            }
192        }
193    }
194
195    private static void fireConsolidatedEvents(List<ListenerInfo> listeners, AbstractDatasetChangedEvent event) {
196        for (ListenerInfo listener: listeners) {
197            if (listener.consolidate) {
198                event.fire(listener.listener);
199            }
200        }
201    }
202
203    @Override
204    public void processDatasetEvent(AbstractDatasetChangedEvent event) {
205        fireEvents(normalListeners, event);
206        eventsInEDT.add(event);
207        SwingUtilities.invokeLater(edtRunnable);
208    }
209}