com.github.rinde.rinsim.ui.SimulationViewer.java Source code

Java tutorial

Introduction

Here is the source code for com.github.rinde.rinsim.ui.SimulationViewer.java

Source

/*
 * Copyright (C) 2011-2016 Rinde van Lon, iMinds-DistriNet, KU Leuven
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.rinde.rinsim.ui;

import static com.google.common.base.Preconditions.checkState;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.TabFolder;
import org.eclipse.swt.widgets.TabItem;
import org.joda.time.Period;
import org.joda.time.format.PeriodFormatter;
import org.joda.time.format.PeriodFormatterBuilder;

import com.github.rinde.rinsim.core.SimulatorAPI;
import com.github.rinde.rinsim.core.model.DependencyProvider;
import com.github.rinde.rinsim.core.model.Model;
import com.github.rinde.rinsim.core.model.ModelBuilder.AbstractModelBuilder;
import com.github.rinde.rinsim.core.model.ModelProvider;
import com.github.rinde.rinsim.core.model.ModelReceiver;
import com.github.rinde.rinsim.core.model.time.ClockController;
import com.github.rinde.rinsim.core.model.time.RealtimeClockController;
import com.github.rinde.rinsim.core.model.time.RealtimeClockController.ClockMode;
import com.github.rinde.rinsim.core.model.time.TickListener;
import com.github.rinde.rinsim.core.model.time.TimeLapse;
import com.github.rinde.rinsim.geom.Point;
import com.github.rinde.rinsim.ui.View.ViewOption;
import com.github.rinde.rinsim.ui.renderers.CanvasRenderer;
import com.github.rinde.rinsim.ui.renderers.PanelRenderer;
import com.github.rinde.rinsim.ui.renderers.Renderer;
import com.github.rinde.rinsim.ui.renderers.ViewPort;
import com.github.rinde.rinsim.ui.renderers.ViewRect;
import com.google.auto.value.AutoValue;
import com.google.common.base.Optional;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;

/**
 * Simulation viewer.
 *
 * @author Bartosz Michalik
 * @author Rinde van Lon
 */
final class SimulationViewer extends Composite
        implements TickListener, ControlListener, PaintListener, SelectionListener, Model<Renderer>, ModelReceiver {
    static final String SPACE = " ";
    static final org.eclipse.swt.graphics.Point START_SCREEN_SIZE = new org.eclipse.swt.graphics.Point(800, 500);
    static final org.eclipse.swt.graphics.Point TIME_LABEL_LOC = new org.eclipse.swt.graphics.Point(50, 10);
    static final String PLAY_LABEL = "&Play\tCtrl+P";
    static final long SLEEP_MS = 30;
    static final long TIME_FORMATTER_THRESHOLD = 200;
    static final String TIME_SEPARATOR = ":";
    static final PeriodFormatter FORMATTER = new PeriodFormatterBuilder().appendDays().appendSeparator(SPACE)
            .minimumPrintedDigits(2).printZeroAlways().appendHours().appendLiteral(TIME_SEPARATOR).appendMinutes()
            .appendLiteral(TIME_SEPARATOR).appendSeconds().toFormatter();

    private static final int MIN_SPEED_UP = 1;
    private static final int MAX_SPEED_UP = 512;
    private static final int MAX_ZOOM_LEVEL = 16;

    boolean firstTime = true;
    final ClockController clock;
    @Nullable
    ViewRect viewRect;
    @Nullable
    Label timeLabel;

    final boolean isRealtime;
    final SimulatorAPI simulator;
    ModelProvider modelProvider;

    private Canvas canvas;
    private org.eclipse.swt.graphics.Point origin;
    private org.eclipse.swt.graphics.Point size;

    @Nullable
    private Image image;
    private final List<PanelRenderer> panelRenderers;
    private final List<CanvasRenderer> canvasRenderers;
    private final boolean autoPlay;
    private MenuItem playPauseMenuItem;
    // multiplier
    private double m;

    @Nullable
    private ScrollBar hBar;
    @Nullable
    private ScrollBar vBar;

    // rendering frequency related
    private int speedUp;
    private long lastRefresh;

    private int zoomRatio;
    private final Display display;
    private final Map<MenuItems, Integer> accelerators;

    SimulationViewer(Shell shell, ClockController cc, SimulatorAPI simapi, View.Builder vb) {
        super(shell, SWT.NONE);

        clock = cc;
        isRealtime = clock instanceof RealtimeClockController;

        simulator = simapi;

        accelerators = vb.accelerators();
        autoPlay = vb.viewOptions().contains(ViewOption.AUTO_PLAY);

        canvasRenderers = new ArrayList<>();
        panelRenderers = new ArrayList<>();

        speedUp = vb.speedUp();
        shell.setLayout(new FillLayout());
        display = shell.getDisplay();
        setLayout(new FillLayout());

        createMenu(shell);
    }

    void show() {
        final Multimap<Integer, PanelRenderer> panels = LinkedHashMultimap.create();
        for (final PanelRenderer pr : panelRenderers) {
            panels.put(pr.getPreferredPosition(), pr);
        }
        panelsLayout(panels);
    }

    void panelsLayout(Multimap<Integer, PanelRenderer> panels) {
        if (panels.isEmpty()) {
            createContent(this);
        } else {

            final SashForm vertical = new SashForm(this, SWT.VERTICAL | SWT.SMOOTH);
            vertical.setLayout(new FillLayout());

            final int topHeight = configurePanels(vertical, panels.removeAll(SWT.TOP));

            final SashForm horizontal = new SashForm(vertical, SWT.HORIZONTAL | SWT.SMOOTH);
            horizontal.setLayout(new FillLayout());

            final int leftWidth = configurePanels(horizontal, panels.removeAll(SWT.LEFT));

            // create canvas
            createContent(horizontal);

            final int rightWidth = configurePanels(horizontal, panels.removeAll(SWT.RIGHT));
            final int bottomHeight = configurePanels(vertical, panels.removeAll(SWT.BOTTOM));

            final int canvasHeight = size.y - topHeight - bottomHeight;
            if (topHeight > 0 && bottomHeight > 0) {
                vertical.setWeights(varargs(topHeight, canvasHeight, bottomHeight));
            } else if (topHeight > 0) {
                vertical.setWeights(varargs(topHeight, canvasHeight));
            } else if (bottomHeight > 0) {
                vertical.setWeights(varargs(canvasHeight, bottomHeight));
            }

            final int canvasWidth = size.x - leftWidth - rightWidth;
            if (leftWidth > 0 && rightWidth > 0) {
                horizontal.setWeights(varargs(leftWidth, canvasWidth, rightWidth));
            } else if (leftWidth > 0) {
                horizontal.setWeights(varargs(leftWidth, canvasWidth));
            } else if (rightWidth > 0) {
                horizontal.setWeights(varargs(canvasWidth, rightWidth));
            }

            checkState(panels.isEmpty(), "Invalid preferred position set for panels: %s", panels.values());
        }
    }

    static int[] varargs(int... ints) {
        return ints;
    }

    int configurePanels(SashForm parent, Collection<PanelRenderer> panels) {
        if (panels.isEmpty()) {
            return 0;
        }

        int prefSize = 0;
        for (final PanelRenderer p : panels) {
            prefSize = Math.max(p.preferredSize(), prefSize);
        }
        if (panels.size() == 1) {
            final PanelRenderer p = panels.iterator().next();
            final Group g = new Group(parent, SWT.SHADOW_NONE);
            p.initializePanel(g);
        } else {
            final TabFolder tab = new TabFolder(parent, SWT.NONE);

            for (final PanelRenderer p : panels) {
                final TabItem ti = new TabItem(tab, SWT.NONE);
                ti.setText(p.getName());
                final Composite comp = new Composite(tab, SWT.NONE);
                ti.setControl(comp);
                p.initializePanel(comp);
            }
        }
        return prefSize;
    }

    /**
     * Configure shell.
     */
    void createContent(Composite parent) {
        canvas = new Canvas(parent,
                SWT.DOUBLE_BUFFERED | SWT.NONE | SWT.NO_REDRAW_RESIZE | SWT.V_SCROLL | SWT.H_SCROLL);
        canvas.setBackground(display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));

        origin = new org.eclipse.swt.graphics.Point(0, 0);
        size = START_SCREEN_SIZE;
        canvas.addPaintListener(this);
        canvas.addControlListener(this);
        this.layout();

        timeLabel = new Label(canvas, SWT.NONE);
        timeLabel.setText("hello world");
        timeLabel.pack();
        timeLabel.setLocation(TIME_LABEL_LOC);
        timeLabel.setBackground(canvas.getDisplay().getSystemColor(SWT.COLOR_WHITE));

        hBar = canvas.getHorizontalBar();
        hBar.addSelectionListener(this);
        vBar = canvas.getVerticalBar();
        vBar.addSelectionListener(this);
    }

    @SuppressWarnings("unused")
    void createMenu(Shell shell) {
        final Menu bar = new Menu(shell, SWT.BAR);
        shell.setMenuBar(bar);

        final MenuItem fileItem = new MenuItem(bar, SWT.CASCADE);
        fileItem.setText("&Control");

        final Menu submenu = new Menu(shell, SWT.DROP_DOWN);
        fileItem.setMenu(submenu);

        // play switch
        playPauseMenuItem = new MenuItem(submenu, SWT.PUSH);
        playPauseMenuItem.setText(PLAY_LABEL);
        playPauseMenuItem.setAccelerator(accelerators.get(MenuItems.PLAY));
        playPauseMenuItem.addListener(SWT.Selection, new Listener() {

            @Override
            public void handleEvent(@Nullable Event e) {
                assert e != null;
                onToglePlay((MenuItem) e.widget);
            }
        });

        new MenuItem(submenu, SWT.SEPARATOR);
        // step execution switch
        final MenuItem nextItem = new MenuItem(submenu, SWT.PUSH);
        nextItem.setText("Next tick\tCtrl+Shift+]");
        nextItem.setAccelerator(accelerators.get(MenuItems.NEXT_TICK));
        nextItem.addListener(SWT.Selection, new Listener() {
            @Override
            public void handleEvent(@Nullable Event e) {
                assert e != null;
                onTick((MenuItem) e.widget);
            }
        });

        // view options

        final MenuItem viewItem = new MenuItem(bar, SWT.CASCADE);
        viewItem.setText("&View");

        final Menu viewMenu = new Menu(shell, SWT.DROP_DOWN);
        viewItem.setMenu(viewMenu);

        // zooming
        final MenuItem zoomInItem = new MenuItem(viewMenu, SWT.PUSH);
        zoomInItem.setText("Zoom &in\tCtrl++");
        zoomInItem.setAccelerator(accelerators.get(MenuItems.ZOOM_IN));
        zoomInItem.setData(MenuItems.ZOOM_IN);

        final MenuItem zoomOutItem = new MenuItem(viewMenu, SWT.PUSH);
        zoomOutItem.setText("Zoom &out\tCtrl+-");
        zoomOutItem.setAccelerator(accelerators.get(MenuItems.ZOOM_OUT));
        zoomOutItem.setData(MenuItems.ZOOM_OUT);

        final Listener zoomingListener = new Listener() {
            @Override
            public void handleEvent(@Nullable Event e) {
                assert e != null;
                onZooming((MenuItem) e.widget);
            }
        };
        zoomInItem.addListener(SWT.Selection, zoomingListener);
        zoomOutItem.addListener(SWT.Selection, zoomingListener);

        // speedUp

        final Listener speedUpListener = new Listener() {

            @Override
            public void handleEvent(@Nullable Event e) {
                assert e != null;
                onSpeedChange((MenuItem) e.widget);
            }
        };

        final MenuItem increaseSpeedItem = new MenuItem(submenu, SWT.PUSH);
        increaseSpeedItem.setAccelerator(accelerators.get(MenuItems.INCREASE_SPEED));
        increaseSpeedItem.setText("Speed &up\tCtrl+]");
        increaseSpeedItem.setData(MenuItems.INCREASE_SPEED);
        increaseSpeedItem.addListener(SWT.Selection, speedUpListener);
        //
        final MenuItem decreaseSpeed = new MenuItem(submenu, SWT.PUSH);
        decreaseSpeed.setAccelerator(accelerators.get(MenuItems.DECREASE_SPEED));
        decreaseSpeed.setText("Slow &down\tCtrl+[");
        decreaseSpeed.setData(MenuItems.DECREASE_SPEED);
        decreaseSpeed.addListener(SWT.Selection, speedUpListener);

    }

    /*
     * Default implementation of the play/pause action. Can be overridden if
     * needed.
     *
     * @param source
     */
    void onToglePlay(MenuItem source) {
        if (clock.isTicking()) {
            source.setText(PLAY_LABEL);
        } else {
            source.setText("&Pause\tCtrl+P");
        }
        new Thread() {
            @Override
            public void run() {
                if (clock.isTicking()) {
                    clock.stop();
                } else {
                    clock.start();
                }
            }
        }.start();
    }

    /*
     * Default implementation of step execution action. Can be overridden if
     * needed.
     *
     * @param source
     */
    void onTick(MenuItem source) {
        if (clock.isTicking()) {
            clock.stop();
        }
        clock.tick();
    }

    void onZooming(MenuItem source) {
        if (source.getData() == MenuItems.ZOOM_IN) {
            if (zoomRatio == MAX_ZOOM_LEVEL) {
                return;
            }
            m *= 2;
            origin.x *= 2;
            origin.y *= 2;
            zoomRatio <<= 1;
        } else {
            if (zoomRatio < 2) {
                return;
            }
            m /= 2;
            origin.x /= 2;
            origin.y /= 2;
            zoomRatio >>= 1;
        }
        if (image != null) {
            image.dispose();
        }
        // this forces a redraw
        image = null;
        canvas.redraw();
    }

    void onSpeedChange(MenuItem source) {
        if (source.getData() == MenuItems.INCREASE_SPEED) {
            if (speedUp < MAX_SPEED_UP) {
                speedUp <<= 1;
            }
        } else {
            if (speedUp > MIN_SPEED_UP) {
                speedUp >>= 1;
            }
        }
    }

    Image renderStatic() {
        size = new org.eclipse.swt.graphics.Point((int) (m * viewRect.width), (int) (m * viewRect.height));
        final Image img = new Image(getDisplay(), size.x, size.y);
        final GC gc = new GC(img);

        for (final CanvasRenderer r : canvasRenderers) {
            r.renderStatic(gc, new ViewPort(new Point(0, 0), viewRect, m));
        }
        gc.dispose();
        return img;
    }

    @Override
    public void paintControl(@Nullable PaintEvent e) {
        assert e != null;
        final GC gc = e.gc;

        final boolean wasFirstTime = firstTime;
        if (firstTime) {
            calculateSizes();
            firstTime = false;
        }

        if (image == null) {
            image = renderStatic();
            updateScrollbars(false);
        }

        final org.eclipse.swt.graphics.Point center = getCenteredOrigin();

        gc.drawImage(image, center.x, center.y);
        for (final CanvasRenderer renderer : canvasRenderers) {
            renderer.renderDynamic(gc, new ViewPort(new Point(center.x, center.y), viewRect, m),
                    clock.getCurrentTime());
        }
        for (final PanelRenderer renderer : panelRenderers) {
            renderer.render();
        }

        final Rectangle content = image.getBounds();
        final Rectangle client = canvas.getClientArea();

        hBar.setVisible(content.width > client.width);
        vBar.setVisible(content.height > client.height);

        // auto play sim if required
        if (wasFirstTime && autoPlay) {
            onToglePlay(playPauseMenuItem);
        }
    }

    org.eclipse.swt.graphics.Point getCenteredOrigin() {
        final Rectangle rect = image.getBounds();
        final Rectangle client = canvas.getClientArea();
        final int zeroX = client.x + client.width / 2 - rect.width / 2;
        final int zeroY = client.y + client.height / 2 - rect.height / 2;
        return new org.eclipse.swt.graphics.Point(origin.x + zeroX, origin.y + zeroY);
    }

    void updateScrollbars(boolean adaptToScrollbar) {
        final Rectangle rect = image.getBounds();
        final Rectangle client = canvas.getClientArea();

        hBar.setMaximum(rect.width);
        vBar.setMaximum(rect.height);
        hBar.setThumb(Math.min(rect.width, client.width));
        vBar.setThumb(Math.min(rect.height, client.height));
        if (!adaptToScrollbar) {
            final org.eclipse.swt.graphics.Point center = getCenteredOrigin();
            hBar.setSelection(-center.x);
            vBar.setSelection(-center.y);
        }
    }

    private void calculateSizes() {
        double minX = Double.POSITIVE_INFINITY;
        double maxX = Double.NEGATIVE_INFINITY;
        double minY = Double.POSITIVE_INFINITY;
        double maxY = Double.NEGATIVE_INFINITY;

        boolean isDefined = false;
        for (final CanvasRenderer r : canvasRenderers) {
            final Optional<ViewRect> rect = r.getViewRect();
            if (rect.isPresent()) {
                minX = Math.min(minX, rect.get().min.x);
                maxX = Math.max(maxX, rect.get().max.x);
                minY = Math.min(minY, rect.get().min.y);
                maxY = Math.max(maxY, rect.get().max.y);
                isDefined = true;
            }
        }

        checkState(isDefined, "none of the available renderers implements getViewRect(), known " + "renderers: %s",
                canvasRenderers);

        viewRect = new ViewRect(new Point(minX, minY), new Point(maxX, maxY));

        final Rectangle area = canvas.getClientArea();
        if (viewRect.width > viewRect.height) {
            m = area.width / viewRect.width;
        } else {
            m = area.height / viewRect.height;
        }
        zoomRatio = 1;
    }

    @Override
    public void controlMoved(ControlEvent e) {
    }

    @Override
    public void controlResized(ControlEvent e) {
        if (image != null) {
            updateScrollbars(true);
            scrollHorizontal();
            scrollVertical();
            canvas.redraw();
        }
    }

    @Override
    public void widgetSelected(SelectionEvent e) {
        if (e.widget == vBar) {
            scrollVertical();
        } else {
            scrollHorizontal();
        }
    }

    void scrollVertical() {
        final org.eclipse.swt.graphics.Point center = getCenteredOrigin();
        final Rectangle content = image.getBounds();
        final Rectangle client = canvas.getClientArea();
        if (client.height > content.height) {
            origin.y = 0;
        } else {
            final int vSelection = vBar.getSelection();
            final int destY = -vSelection - center.y;
            canvas.scroll(center.x, destY, center.x, center.y, content.width, content.height, false);
            origin.y = -vSelection + origin.y - center.y;
        }
    }

    void scrollHorizontal() {
        final org.eclipse.swt.graphics.Point center = getCenteredOrigin();
        final Rectangle content = image.getBounds();
        final Rectangle client = canvas.getClientArea();
        if (client.width > content.width) {
            origin.x = 0;
        } else {
            final int hSelection = hBar.getSelection();
            final int destX = -hSelection - center.x;
            canvas.scroll(destX, center.y, center.x, center.y, content.width, content.height, false);
            origin.x = -hSelection + origin.x - center.x;
        }
    }

    @Override
    public void widgetDefaultSelected(SelectionEvent e) {
    }

    @Override
    public void tick(TimeLapse timeLapse) {
    }

    @Override
    public void afterTick(final TimeLapse timeLapse) {
        if (clock.isTicking()
                // when in realtime mode ignore the gui speed up
                && !(isRealtime && ((RealtimeClockController) clock).getClockMode() == ClockMode.REAL_TIME)
                && lastRefresh + timeLapse.getTickLength() * speedUp > timeLapse.getStartTime()) {
            return;
        }
        lastRefresh = timeLapse.getStartTime();
        // TODO sleep should be relative to speedUp as well?
        if (!isRealtime) {
            try {
                Thread.sleep(SLEEP_MS);
            } catch (final InterruptedException e) {
                throw new RuntimeException(e);
            }
            if (display.isDisposed()) {
                return;
            }
        }
        display.syncExec(new Runnable() {
            @Override
            public void run() {
                if (!canvas.isDisposed()) {
                    if (clock.getTickLength() > TIME_FORMATTER_THRESHOLD) {
                        final StringBuilder sb = new StringBuilder();
                        sb.append(FORMATTER.print(new Period(0, clock.getCurrentTime())));

                        if (isRealtime) {
                            sb.append(SPACE);
                            sb.append(((RealtimeClockController) clock).getClockMode().name());
                        }
                        timeLabel.setText(sb.toString());
                    } else {
                        timeLabel.setText(Long.toString(clock.getCurrentTime()));
                    }
                    timeLabel.pack();
                    canvas.redraw();
                }
            }
        });
    }

    @Override
    public boolean register(Renderer element) {
        if (element instanceof PanelRenderer) {
            panelRenderers.add((PanelRenderer) element);
        }
        if (element instanceof CanvasRenderer) {
            canvasRenderers.add((CanvasRenderer) element);
        }
        return true;
    }

    @Override
    public boolean unregister(Renderer element) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Class<Renderer> getSupportedType() {
        return Renderer.class;
    }

    @Override
    public void registerModelProvider(ModelProvider mp) {
        modelProvider = mp;
    }

    @Override
    public <U> U get(Class<U> clazz) {
        throw new UnsupportedOperationException();
    }

    static Builder builder(View.Builder vb) {
        return new AutoValue_SimulationViewer_Builder(vb);
    }

    @AutoValue
    abstract static class Builder extends AbstractModelBuilder<SimulationViewer, Renderer> {

        Builder() {
            setDependencies(Shell.class, ClockController.class, SimulatorAPI.class, MainView.class);
        }

        abstract View.Builder viewBuilder();

        @Override
        public SimulationViewer build(DependencyProvider dependencyProvider) {
            final Shell shell = dependencyProvider.get(Shell.class);
            final ClockController cc = dependencyProvider.get(ClockController.class);
            final SimulatorAPI sim = dependencyProvider.get(SimulatorAPI.class);
            final MainView mv = dependencyProvider.get(MainView.class);
            final SimulationViewer sv = new SimulationViewer(shell, cc, sim, viewBuilder());
            mv.addListener(new com.github.rinde.rinsim.event.Listener() {
                @Override
                public void handleEvent(com.github.rinde.rinsim.event.Event e) {
                    sv.show();
                }
            });
            return sv;
        }
    }
}