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