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