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