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