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}