/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.g2d.diagram.participant; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.simantics.g2d.canvas.Hints; import org.simantics.g2d.canvas.ICanvasContext; import org.simantics.g2d.diagram.IDiagram; import org.simantics.g2d.diagram.IDiagram.CompositionListener; import org.simantics.g2d.diagram.handler.PickRequest.PickFilter; import org.simantics.g2d.element.IElement; import org.simantics.g2d.internal.DebugPolicy; import org.simantics.utils.datastructures.hints.IHintContext.Key; import org.simantics.utils.datastructures.hints.IHintListener; import org.simantics.utils.datastructures.hints.IHintObservable; import org.simantics.utils.threads.ThreadUtils; /** * A diagram participant that is meant for keeping a selected set of diagram * elements up-to-date. The difference to {@link ElementPainter} is that this * participant throttles updates so that the selected set of elements is updated * once in a specified time interval. * *

* This class listens to {@link Hints#KEY_DIRTY} hint of elements accepted by * the specified {@link #elementFilter}. If the key receives a value of * {@link Hints#VALUE_SG_DELAYED_UPDATE}, this class will react by queueing the * element to be updated after the current delay period is over. The delay * period ensures that existing elements are not updated more frequently than * the specified time period. When it becomes time to perform the actual * updates, this class will set the {@link Hints#KEY_DIRTY} hint of all * currently dirty elements to value {@link Hints#VALUE_SG_DIRTY}. This will * signal other listeners ({@link ElementPainter}) to react. * * @author Tuukka Lehtonen */ public class DelayedBatchElementPainter extends AbstractDiagramParticipant { private static final boolean DEBUG = DebugPolicy.DEBUG_DELAYED_ELEMENT_PAINTER; private static final boolean DEBUG_MARKING = DebugPolicy.DEBUG_DELAYED_ELEMENT_PAINTER_MARKING; ScheduledExecutorService executor = ThreadUtils.getNonBlockingWorkExecutor(); PickFilter elementFilter; long delay; TimeUnit delayUnit; Set dirtyElements = new HashSet(); volatile ScheduledFuture scheduled = null; long lastUpdateTime = 0; class ElementListener implements CompositionListener, IHintListener { @Override public void onElementAdded(IDiagram d, IElement e) { if (DEBUG) debug("onElementAdded(%s, %s)\n", d, e); if (elementFilter.accept(e)) addElement(e); } @Override public void onElementRemoved(IDiagram d, IElement e) { if (DEBUG) debug("onElementRemoved(%s, %s)\n", d, e); if (elementFilter.accept(e)) removeElement(e); } @Override public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) { if (key == Hints.KEY_DIRTY && newValue == Hints.VALUE_SG_DELAYED_UPDATE && sender instanceof IElement) { markDirty((IElement) sender); } } @Override public void hintRemoved(IHintObservable sender, Key key, Object oldValue) { } } private final ElementListener elementListener = new ElementListener(); public DelayedBatchElementPainter(PickFilter elementFilter, long delay, TimeUnit delayUnit) { this.elementFilter = elementFilter; this.delay = delay; this.delayUnit = delayUnit; } @Override public void removedFromContext(ICanvasContext ctx) { ScheduledFuture s = scheduled; if (s != null) s.cancel(false); super.removedFromContext(ctx); } @Override protected void onDiagramSet(IDiagram newValue, IDiagram oldValue) { if (oldValue == newValue) return; if (oldValue != null) { for (IElement e : oldValue.getElements()) { if (elementFilter.accept(e)) removeElement(e); } oldValue.removeCompositionListener(elementListener); } if (newValue != null) { for (IElement e : newValue.getElements()) { if (elementFilter.accept(e)) addElement(e); } newValue.addCompositionListener(elementListener); } } protected void addElement(IElement e) { if (DEBUG) debug("addElement(%s)\n", e); e.addKeyHintListener(Hints.KEY_DIRTY, elementListener); } protected void removeElement(IElement e) { if (DEBUG) debug("removeElement(%s)\n", e); e.removeKeyHintListener(Hints.KEY_DIRTY, elementListener); } protected void markDirty(IElement e) { if (DEBUG_MARKING) debug("Marking element dirty: %s\n", e); dirtyElements.add(e); scheduleUpdate(); } private Runnable updater = new Runnable() { @Override public void run() { if (DEBUG) debug("marking %d elements dirty\n", dirtyElements.size()); for (IElement e : dirtyElements) e.setHint(Hints.KEY_DIRTY, Hints.VALUE_SG_DIRTY); dirtyElements.clear(); scheduled = null; lastUpdateTime = System.currentTimeMillis(); if (DEBUG) debug("marking last update time %d\n", lastUpdateTime); if (!isRemoved()) setDirty(); } }; private Runnable delayedUpdater = new Runnable() { @Override public void run() { if (DEBUG) debug("scheduling updater\n"); asyncExec(updater); } }; private void scheduleUpdate() { if (scheduled == null) { long timeNow = System.currentTimeMillis(); long timeSinceLastUpdate = timeNow - lastUpdateTime; long requestedDelay = delayUnit.toMillis(delay); long scheduleDelay = Math.max(0, Math.min(requestedDelay, requestedDelay - timeSinceLastUpdate)); if (DEBUG) debug("scheduling update with delay %dms (time=%d, time passed=%dms)\n", scheduleDelay, timeNow, timeSinceLastUpdate); if (scheduleDelay == 0) { asyncExec(updater); } else { scheduled = executor.schedule(delayedUpdater, scheduleDelay, TimeUnit.MILLISECONDS); } } } private void debug(String format, Object... args) { if (DEBUG) { System.out.format(getClass().getSimpleName() + "[filter=" + elementFilter + ", delay=" + delayUnit.toMillis(delay) + "ms] " + format, args); } } }