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