/*******************************************************************************
* 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);
}
}
}