001/* 002 * Gridarta MMORPG map editor for Crossfire, Daimonin and similar games. 003 * Copyright (C) 2000-2011 The Gridarta Developers. 004 * 005 * This program is free software; you can redistribute it and/or modify 006 * it under the terms of the GNU General Public License as published by 007 * the Free Software Foundation; either version 2 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU General Public License for more details. 014 * 015 * You should have received a copy of the GNU General Public License along 016 * with this program; if not, write to the Free Software Foundation, Inc., 017 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 018 */ 019 020package net.sf.gridarta.gui.delayedmapmodel; 021 022import java.io.File; 023import java.lang.reflect.InvocationTargetException; 024import java.util.IdentityHashMap; 025import java.util.Map; 026import java.util.Set; 027import javax.swing.SwingUtilities; 028import net.sf.gridarta.model.archetype.Archetype; 029import net.sf.gridarta.model.gameobject.GameObject; 030import net.sf.gridarta.model.maparchobject.MapArchObject; 031import net.sf.gridarta.model.maparchobject.MapArchObjectListener; 032import net.sf.gridarta.model.mapcontrol.MapControl; 033import net.sf.gridarta.model.mapmanager.MapManager; 034import net.sf.gridarta.model.mapmanager.MapManagerListener; 035import net.sf.gridarta.model.mapmodel.MapModel; 036import net.sf.gridarta.model.mapmodel.MapModelListener; 037import net.sf.gridarta.model.mapmodel.MapSquare; 038import net.sf.gridarta.model.validation.ErrorCollector; 039import net.sf.gridarta.utils.EventListenerList2; 040import net.sf.gridarta.utils.Exiter; 041import net.sf.gridarta.utils.ExiterListener; 042import net.sf.gridarta.utils.Size2D; 043import org.apache.log4j.Category; 044import org.apache.log4j.Logger; 045import org.jetbrains.annotations.NotNull; 046import org.jetbrains.annotations.Nullable; 047 048/** 049 * Provides support for delayed notification of {@link MapModel} changes. 050 * @author Andreas Kirschbaum 051 */ 052public class DelayedMapModelListenerManager<G extends GameObject<G, A, R>, A extends MapArchObject<A>, R extends Archetype<G, A, R>> { 053 054 /** 055 * The Logger for printing log messages. 056 */ 057 @NotNull 058 private static final Category log = Logger.getLogger(DelayedMapModelListenerManager.class); 059 060 /** 061 * Notification delay in milliseconds. All listeners will be notified this 062 * delay after the last map change has happened. 063 */ 064 private static final long DELAY = 500L; 065 066 /** 067 * The {@link MapManager} to track. 068 */ 069 @NotNull 070 private final MapManager<G, A, R> mapManager; 071 072 /** 073 * All known {@link MapModel} instances. Maps map model instance to attached 074 * {@link MapModelListenerImpl} instance. 075 */ 076 @NotNull 077 private final Map<MapModel<G, A, R>, MapModelListenerImpl> mapModelListeners = new IdentityHashMap<MapModel<G, A, R>, MapModelListenerImpl>(); 078 079 /** 080 * All known {@link MapModel} instances. Maps map model instance to attached 081 * {@link MapArchObjectListenerImpl} instance. 082 */ 083 @NotNull 084 private final Map<MapModel<G, A, R>, MapArchObjectListenerImpl> mapArchObjectListeners = new IdentityHashMap<MapModel<G, A, R>, MapArchObjectListenerImpl>(); 085 086 /** 087 * The timestamp when to deliver scheduled map models. Set to zero when not 088 * active. 089 */ 090 private long timeout; 091 092 /** 093 * The object used for synchronization. 094 */ 095 @NotNull 096 private final Object sync = new Object(); 097 098 /** 099 * The {@link MapModel MapModels} having pending changes. Accesses are 100 * synchronized with {@link #sync}. 101 */ 102 @NotNull 103 private final Map<MapModel<G, A, R>, Void> scheduledMapModels = new IdentityHashMap<MapModel<G, A, R>, Void>(); 104 105 /** 106 * The listeners to notify. 107 */ 108 @NotNull 109 private final EventListenerList2<DelayedMapModelListener<G, A, R>> listeners = new EventListenerList2<DelayedMapModelListener<G, A, R>>(DelayedMapModelListener.class); 110 111 /** 112 * The {@link MapManagerListener} used to detect created and closed {@link 113 * MapControl} instances. 114 */ 115 @NotNull 116 private final MapManagerListener<G, A, R> mapManagerListener = new MapManagerListener<G, A, R>() { 117 118 @Override 119 public void currentMapChanged(@Nullable final MapControl<G, A, R> mapControl) { 120 // ignore 121 } 122 123 @Override 124 public void mapCreated(@NotNull final MapControl<G, A, R> mapControl, final boolean interactive) { 125 final MapModel<G, A, R> mapModel = mapControl.getMapModel(); 126 final MapModelListenerImpl mapModelListener = new MapModelListenerImpl(mapModel); 127 mapModel.addMapModelListener(mapModelListener); 128 mapModelListeners.put(mapModel, mapModelListener); 129 final MapArchObjectListenerImpl mapArchObjectListener = new MapArchObjectListenerImpl(mapModel); 130 mapModel.getMapArchObject().addMapArchObjectListener(mapArchObjectListener); 131 mapArchObjectListeners.put(mapModel, mapArchObjectListener); 132 scheduleMapModel(mapModel); 133 } 134 135 @Override 136 public void mapClosing(@NotNull final MapControl<G, A, R> mapControl) { 137 // ignore 138 } 139 140 @Override 141 public void mapClosed(@NotNull final MapControl<G, A, R> mapControl) { 142 final MapModel<G, A, R> mapModel = mapControl.getMapModel(); 143 final MapModelListener<G, A, R> mapModelListener = mapModelListeners.remove(mapModel); 144 if (mapModelListener != null) { 145 mapModel.removeMapModelListener(mapModelListener); 146 } 147 final MapArchObjectListener mapArchObjectListener = mapArchObjectListeners.remove(mapModel); 148 if (mapArchObjectListener != null) { 149 mapModel.getMapArchObject().removeMapArchObjectListener(mapArchObjectListener); 150 } 151 synchronized (sync) { 152 scheduledMapModels.remove(mapControl.getMapModel()); 153 } 154 } 155 156 }; 157 158 /** 159 * The {@link Thread} performing map validations. 160 */ 161 private final Thread thread = new Thread(new Runnable() { 162 163 @Override 164 public void run() { 165 try { 166 final Map<MapModel<G, A, R>, Void> mapModels = new IdentityHashMap<MapModel<G, A, R>, Void>(); 167 while (!Thread.currentThread().isInterrupted()) { 168 while (true) { 169 final long now = System.currentTimeMillis(); 170 synchronized (sync) { 171 if (timeout == 0L) { 172 sync.wait(); 173 } else { 174 final long diff = timeout - now; 175 if (diff <= 0L) { 176 timeout = 0L; 177 mapModels.putAll(scheduledMapModels); 178 scheduledMapModels.clear(); 179 break; 180 } 181 182 sync.wait(diff); 183 } 184 } 185 } 186 187 try { 188 SwingUtilities.invokeAndWait(new Runnable() { 189 190 @Override 191 public void run() { 192 for (final Map.Entry<MapModel<G, A, R>, Void> e : mapModels.entrySet()) { 193 final MapModel<G, A, R> mapModel = e.getKey(); 194 for (final DelayedMapModelListener<G, A, R> listener : listeners.getListeners()) { 195 listener.mapModelChanged(mapModel); 196 } 197 } 198 } 199 }); 200 } catch (final InvocationTargetException ex) { 201 log.error("InvocationTargetException: " + ex.getMessage(), ex); 202 } 203 mapModels.clear(); 204 } 205 } catch (final InterruptedException ignored) { 206 Thread.currentThread().interrupt(); 207 } 208 } 209 210 }); 211 212 /** 213 * Creates a new instance. 214 * @param mapManager the map manager to track 215 * @param exiter the exiter instance 216 */ 217 public DelayedMapModelListenerManager(@NotNull final MapManager<G, A, R> mapManager, @NotNull final Exiter exiter) { 218 this.mapManager = mapManager; 219 mapManager.addMapManagerListener(mapManagerListener); 220 221 final ExiterListener exiterListener = new ExiterListener() { 222 223 @Override 224 public void preExitNotify() { 225 thread.interrupt(); 226 try { 227 thread.join(); 228 } catch (final InterruptedException ignored) { 229 Thread.currentThread().interrupt(); 230 log.warn("DelayedMapModelListenerManager was interrupted"); 231 } 232 } 233 234 @Override 235 public void appExitNotify() { 236 // ignore 237 } 238 239 @Override 240 public void waitExitNotify() { 241 // ignore 242 } 243 244 }; 245 exiter.addExiterListener(exiterListener); 246 } 247 248 /** 249 * Starts execution. 250 */ 251 public void start() { 252 thread.start(); 253 scheduleAllMapModels(); 254 } 255 256 /** 257 * Adds a {@link DelayedMapModelListener} to be notified. 258 * @param listener the listener 259 */ 260 public void addDelayedMapModelListener(@NotNull final DelayedMapModelListener<G, A, R> listener) { 261 listeners.add(listener); 262 for (final MapControl<G, A, R> mapControl : mapManager.getOpenedMaps()) { 263 listener.mapModelChanged(mapControl.getMapModel()); 264 } 265 } 266 267 /** 268 * Removes a {@link DelayedMapModelListener} to be notified. 269 * @param listener the listener 270 */ 271 public void removeDelayedMapModelListener(@NotNull final DelayedMapModelListener<G, A, R> listener) { 272 listeners.remove(listener); 273 } 274 275 /** 276 * Schedules a {@link MapModel} which has been changed. 277 * @param mapModel the map model to schedule 278 */ 279 public void scheduleMapModel(@NotNull final MapModel<G, A, R> mapModel) { 280 final long now = System.currentTimeMillis(); 281 synchronized (sync) { 282 scheduledMapModels.put(mapModel, null); 283 timeout = now + DELAY; 284 sync.notifyAll(); 285 } 286 } 287 288 /** 289 * Schedules all {@link MapModel MapModels} as changed. 290 */ 291 public void scheduleAllMapModels() { 292 final long now = System.currentTimeMillis(); 293 synchronized (sync) { 294 for (final MapControl<G, A, R> mapControl : mapManager.getOpenedMaps()) { 295 scheduledMapModels.put(mapControl.getMapModel(), null); 296 } 297 timeout = now + DELAY; 298 sync.notifyAll(); 299 } 300 } 301 302 /** 303 * A {@link MapModelListener} attached to all existing maps. It calls {@link 304 * DelayedMapModelListenerManager#scheduleMapModel(MapModel)} for all map 305 * changes. 306 */ 307 private class MapModelListenerImpl implements MapModelListener<G, A, R> { 308 309 /** 310 * The tracked {@link MapModel} instance. 311 */ 312 private final MapModel<G, A, R> mapModel; 313 314 /** 315 * Creates a new instance. 316 * @param mapModel the map model to track 317 */ 318 private MapModelListenerImpl(@NotNull final MapModel<G, A, R> mapModel) { 319 this.mapModel = mapModel; 320 } 321 322 @Override 323 public void mapSizeChanged(@NotNull final Size2D newSize) { 324 scheduleMapModel(mapModel); 325 } 326 327 @Override 328 public void mapSquaresChanged(@NotNull final Set<MapSquare<G, A, R>> mapSquares) { 329 scheduleMapModel(mapModel); 330 } 331 332 @Override 333 public void mapObjectsChanged(@NotNull final Set<G> gameObjects, @NotNull final Set<G> transientGameObjects) { 334 scheduleMapModel(mapModel); 335 } 336 337 @Override 338 public void errorsChanged(@NotNull final ErrorCollector<G, A, R> errors) { 339 // ignore 340 } 341 342 @Override 343 public void mapFileChanged(@Nullable final File oldMapFile) { 344 scheduleMapModel(mapModel); 345 } 346 347 @Override 348 public void modifiedChanged() { 349 // ignore 350 } 351 352 } 353 354 /** 355 * A {@link MapArchObjectListener} attached to all existing maps. It calls 356 * {@link DelayedMapModelListenerManager#scheduleMapModel(MapModel)} for all 357 * map changes. 358 */ 359 private class MapArchObjectListenerImpl implements MapArchObjectListener { 360 361 /** 362 * The tracked {@link MapModel} instance. 363 */ 364 private final MapModel<G, A, R> mapModel; 365 366 /** 367 * Creates a new instance. 368 * @param mapModel the map model to track 369 */ 370 private MapArchObjectListenerImpl(@NotNull final MapModel<G, A, R> mapModel) { 371 this.mapModel = mapModel; 372 } 373 374 @Override 375 public void mapMetaChanged() { 376 scheduleMapModel(mapModel); 377 } 378 379 @Override 380 public void mapSizeChanged(@NotNull final Size2D mapSize) { 381 // ignore 382 } 383 384 } 385 386}