// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.osm.event;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingQueue;
import javax.swing.SwingUtilities;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener;
import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
/**
* This class allows to add DatasetListener to currently active dataset. If active
* layer is changed, listeners are automatically registered at new active dataset
* (it's no longer necessary to register for layer events and reregister every time
* new layer is selected)
*
* Events in EDT are supported, see {@link #addDatasetListener(DataSetListener, FireMode)}
*
*/
public class DatasetEventManager implements ActiveLayerChangeListener, Listener {
private static final DatasetEventManager instance = new DatasetEventManager();
private final class EdtRunnable implements Runnable {
@Override
public void run() {
while (!eventsInEDT.isEmpty()) {
DataSet dataSet = null;
AbstractDatasetChangedEvent consolidatedEvent = null;
AbstractDatasetChangedEvent event;
while ((event = eventsInEDT.poll()) != null) {
fireEvents(inEDTListeners, event);
// DataSet changed - fire consolidated event early
if (consolidatedEvent != null && dataSet != event.getDataset()) {
fireConsolidatedEvents(inEDTListeners, consolidatedEvent);
consolidatedEvent = null;
}
dataSet = event.getDataset();
// Build consolidated event
if (event instanceof DataChangedEvent) {
// DataChangeEvent can contains other events, so it gets special handling
DataChangedEvent dataEvent = (DataChangedEvent) event;
if (dataEvent.getEvents() == null) {
consolidatedEvent = dataEvent; // Dataset was completely changed, we can ignore older events
} else {
if (consolidatedEvent == null) {
consolidatedEvent = new DataChangedEvent(dataSet, dataEvent.getEvents());
} else if (consolidatedEvent instanceof DataChangedEvent) {
List<AbstractDatasetChangedEvent> evts = ((DataChangedEvent) consolidatedEvent).getEvents();
if (evts != null) {
evts.addAll(dataEvent.getEvents());
}
} else {
AbstractDatasetChangedEvent oldConsolidateEvent = consolidatedEvent;
consolidatedEvent = new DataChangedEvent(dataSet, dataEvent.getEvents());
((DataChangedEvent) consolidatedEvent).getEvents().add(oldConsolidateEvent);
}
}
} else {
// Normal events
if (consolidatedEvent == null) {
consolidatedEvent = event;
} else if (consolidatedEvent instanceof DataChangedEvent) {
List<AbstractDatasetChangedEvent> evs = ((DataChangedEvent) consolidatedEvent).getEvents();
if (evs != null) {
evs.add(event);
}
} else {
consolidatedEvent = new DataChangedEvent(dataSet, new ArrayList<>(Arrays.asList(consolidatedEvent)));
}
}
}
// Fire consolidated event
if (consolidatedEvent != null) {
fireConsolidatedEvents(inEDTListeners, consolidatedEvent);
}
}
}
}
public enum FireMode {
/**
* Fire in calling thread immediately.
*/
IMMEDIATELY,
/**
* Fire in event dispatch thread.
*/
IN_EDT,
/**
* Fire in event dispatch thread. If more than one event arrived when event queue is checked, merged them to one event
*/
IN_EDT_CONSOLIDATED
}
private static class ListenerInfo {
private final DataSetListener listener;
private final boolean consolidate;
ListenerInfo(DataSetListener listener, boolean consolidate) {
this.listener = listener;
this.consolidate = consolidate;
}
@Override
public int hashCode() {
return Objects.hash(listener);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ListenerInfo that = (ListenerInfo) o;
return Objects.equals(listener, that.listener);
}
}
/**
* Replies the unique instance.
* @return the unique instance
*/
public static DatasetEventManager getInstance() {
return instance;
}
private final Queue<AbstractDatasetChangedEvent> eventsInEDT = new LinkedBlockingQueue<>();
private final CopyOnWriteArrayList<ListenerInfo> inEDTListeners = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<ListenerInfo> normalListeners = new CopyOnWriteArrayList<>();
private final DataSetListener myListener = new DataSetListenerAdapter(this);
private final Runnable edtRunnable = new EdtRunnable();
/**
* Constructs a new {@code DatasetEventManager}.
*/
public DatasetEventManager() {
Main.getLayerManager().addActiveLayerChangeListener(this);
}
/**
* Register listener, that will receive events from currently active dataset
* @param listener the listener to be registered
* @param fireMode If {@link FireMode#IN_EDT} or {@link FireMode#IN_EDT_CONSOLIDATED},
* listener will be notified in event dispatch thread instead of thread that caused
* the dataset change
*/
public void addDatasetListener(DataSetListener listener, FireMode fireMode) {
if (fireMode == FireMode.IN_EDT || fireMode == FireMode.IN_EDT_CONSOLIDATED) {
inEDTListeners.addIfAbsent(new ListenerInfo(listener, fireMode == FireMode.IN_EDT_CONSOLIDATED));
} else {
normalListeners.addIfAbsent(new ListenerInfo(listener, false));
}
}
public void removeDatasetListener(DataSetListener listener) {
ListenerInfo searchListener = new ListenerInfo(listener, false);
inEDTListeners.remove(searchListener);
normalListeners.remove(searchListener);
}
@Override
public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
DataSet oldData = e.getPreviousEditDataSet();
if (oldData != null) {
oldData.removeDataSetListener(myListener);
}
DataSet newData = e.getSource().getEditDataSet();
if (newData != null) {
newData.addDataSetListener(myListener);
}
processDatasetEvent(new DataChangedEvent(newData));
}
private static void fireEvents(List<ListenerInfo> listeners, AbstractDatasetChangedEvent event) {
for (ListenerInfo listener: listeners) {
if (!listener.consolidate) {
event.fire(listener.listener);
}
}
}
private static void fireConsolidatedEvents(List<ListenerInfo> listeners, AbstractDatasetChangedEvent event) {
for (ListenerInfo listener: listeners) {
if (listener.consolidate) {
event.fire(listener.listener);
}
}
}
@Override
public void processDatasetEvent(AbstractDatasetChangedEvent event) {
fireEvents(normalListeners, event);
eventsInEDT.add(event);
SwingUtilities.invokeLater(edtRunnable);
}
}