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