com.github.rinde.rinsim.pdptw.common.TimeLinePanel.java Source code

Java tutorial

Introduction

Here is the source code for com.github.rinde.rinsim.pdptw.common.TimeLinePanel.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.pdptw.common;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Verify.verify;
import static com.google.common.base.Verify.verifyNotNull;
import static com.google.common.collect.Lists.newArrayList;

import java.math.RoundingMode;
import java.util.List;

import javax.annotation.Nullable;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.ScrollBar;
import org.joda.time.Period;
import org.joda.time.format.PeriodFormatter;
import org.joda.time.format.PeriodFormatterBuilder;

import com.github.rinde.rinsim.core.model.DependencyProvider;
import com.github.rinde.rinsim.core.model.Model.AbstractModelVoid;
import com.github.rinde.rinsim.core.model.ModelBuilder.AbstractModelBuilder;
import com.github.rinde.rinsim.core.model.pdp.PDPModel;
import com.github.rinde.rinsim.core.model.pdp.PDPModel.PDPModelEventType;
import com.github.rinde.rinsim.core.model.pdp.PDPModelEvent;
import com.github.rinde.rinsim.core.model.pdp.Parcel;
import com.github.rinde.rinsim.core.model.time.TickListener;
import com.github.rinde.rinsim.core.model.time.TimeLapse;
import com.github.rinde.rinsim.event.Event;
import com.github.rinde.rinsim.event.Listener;
import com.github.rinde.rinsim.ui.renderers.PanelRenderer;
import com.github.rinde.rinsim.util.TimeWindow;
import com.google.auto.value.AutoValue;
import com.google.common.base.Optional;
import com.google.common.math.DoubleMath;

/**
 * Time line panel is an UI element that shows a real time visualization of
 * parcels and their time windows in a simulation.
 * @author Rinde van Lon
 */
public final class TimeLinePanel extends AbstractModelVoid implements PanelRenderer, TickListener {

    static final PeriodFormatter FORMATTER = new PeriodFormatterBuilder().minimumPrintedDigits(2).printZeroAlways()
            .appendHours().appendLiteral(":").appendMinutes().toFormatter();

    static final int PANEL_PX = 200;
    static final int MARGIN_PX = 2;
    static final int BAR_HEIGHT_PX = 22;
    static final int WIDTH_PX = 700;
    static final int FONT_SIZE = 10;
    static final long TIME_PER_PIXEL = 15000;
    static final int V_THUMB_SIZE = 5;
    static final int H_THUMB_SIZE = 20;

    static final int SCROLL_INCR = 2;
    static final int SCROLL_PAGE_INCR = 20;

    private static final boolean IS_MAC_OR_WINDOWS;

    static {
        final String name = System.getProperty("os.name").toLowerCase();
        IS_MAC_OR_WINDOWS = name.contains("win") || name.contains("mac");
    }

    Optional<Canvas> canvas;
    Optional<Canvas> barCanvas;
    Point origin = new Point(0, 0);
    long currentTime;

    final PDPModel pdpModel;

    TimeLinePanel(PDPModel pm) {
        pdpModel = pm;
        canvas = Optional.absent();
        barCanvas = Optional.absent();
    }

    @Override
    public void initializePanel(Composite parent) {
        final TimelineBar timelineBar = new TimelineBar(parent.getDisplay());
        final Timeline timeline = new Timeline(parent.getDisplay());
        pdpModel.getEventAPI().addListener(new Listener() {
            @Override
            public void handleEvent(Event e) {
                if (e.getEventType() == PDPModelEventType.NEW_PARCEL) {
                    verify(e instanceof PDPModelEvent);

                    final PDPModelEvent event = (PDPModelEvent) e;
                    timeline.addParcel(new ParcelInfo(event.time, verifyNotNull(event.parcel)));
                }
            }
        }, PDPModelEventType.NEW_PARCEL);

        final GridLayout layout = new GridLayout(1, false);
        layout.marginHeight = MARGIN_PX;
        layout.marginWidth = MARGIN_PX;
        layout.verticalSpacing = 0;
        parent.setLayout(layout);
        barCanvas = Optional.of(new Canvas(parent, SWT.NONE));
        final GridData barData = new GridData(SWT.FILL, SWT.TOP, true, false);
        barData.minimumHeight = BAR_HEIGHT_PX;
        barData.heightHint = BAR_HEIGHT_PX;
        barCanvas.get().setLayoutData(barData);
        barCanvas.get().addPaintListener(new PaintListener() {
            @Override
            public void paintControl(@Nullable PaintEvent e) {
                assert e != null;
                timelineBar.update(timeline.getWidth());
                e.gc.setBackground(e.display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
                e.gc.fillRectangle(0, 0, barCanvas.get().getClientArea().width,
                        barCanvas.get().getClientArea().height);

                e.gc.drawImage(timelineBar.contents, origin.x, 0);
                e.gc.setForeground(e.display.getSystemColor(SWT.COLOR_RED));
                e.gc.drawLine(origin.x + (int) (currentTime / TIME_PER_PIXEL), FONT_SIZE,
                        origin.x + (int) (currentTime / TIME_PER_PIXEL), barCanvas.get().getClientArea().height);
            }
        });
        canvas = Optional.of(new Canvas(parent, SWT.DOUBLE_BUFFERED | SWT.NONE | SWT.V_SCROLL | SWT.H_SCROLL));
        final ScrollBar hBar = canvas.get().getHorizontalBar();
        final ScrollBar vBar = canvas.get().getVerticalBar();

        canvas.get().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
        canvas.get().addPaintListener(new PaintListener() {
            @Override
            public void paintControl(@Nullable PaintEvent e) {
                assert e != null;
                final int timeX = DoubleMath.roundToInt(origin.x + currentTime / TIME_PER_PIXEL,
                        RoundingMode.HALF_UP);

                final int height = timeline.getHeight();
                timeline.update(timeX);

                final boolean shouldScroll = timeline.getHeight() > height
                        && vBar.getMaximum() == vBar.getSelection() + vBar.getThumb();

                e.gc.setBackground(e.display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
                e.gc.fillRectangle(0, 0, canvas.get().getClientArea().width, canvas.get().getClientArea().height);
                e.gc.drawImage(timeline.contents.get(), origin.x, origin.y);
                e.gc.setForeground(e.display.getSystemColor(SWT.COLOR_RED));
                e.gc.drawLine(timeX, 0, timeX, canvas.get().getClientArea().height);
                hBar.setMaximum(timeline.getWidth() == 0 ? 1 : timeline.getWidth() + H_THUMB_SIZE);
                vBar.setMaximum(timeline.getHeight() + V_THUMB_SIZE);
                hBar.setThumb(Math.min(timeline.getWidth() + H_THUMB_SIZE, canvas.get().getClientArea().width));
                vBar.setThumb(Math.min(timeline.getHeight() + V_THUMB_SIZE, canvas.get().getClientArea().height));

                // if view is currently scrolled down, automatically scroll down when
                // view is expanded downward (similar to the behavior of a terminal)
                if (shouldScroll) {
                    vBar.setSelection(vBar.getMaximum());
                    final int vSelection = vBar.getSelection();
                    final int destY = -vSelection - origin.y;
                    canvas.get().scroll(0, destY, 0, 0, timeline.getWidth(), timeline.getHeight(), false);
                    origin.y = -vSelection;
                }
            }
        });

        hBar.setIncrement(SCROLL_INCR);
        hBar.setPageIncrement(SCROLL_PAGE_INCR);
        hBar.addListener(SWT.Selection, new org.eclipse.swt.widgets.Listener() {
            @Override
            public void handleEvent(@Nullable org.eclipse.swt.widgets.Event e) {
                final int hSelection = hBar.getSelection();
                final int destX = -hSelection - origin.x;
                canvas.get().scroll(destX, 0, 0, 0, timeline.getWidth(), timeline.getHeight(), false);
                barCanvas.get().scroll(destX, 0, 0, 0, timelineBar.contents.getBounds().width,
                        timelineBar.contents.getBounds().height, false);
                origin.x = -hSelection;
            }
        });
        vBar.setIncrement(SCROLL_INCR);
        vBar.setPageIncrement(SCROLL_PAGE_INCR);
        vBar.addListener(SWT.Selection, new org.eclipse.swt.widgets.Listener() {
            @Override
            public void handleEvent(@Nullable org.eclipse.swt.widgets.Event e) {
                final int vSelection = vBar.getSelection();
                final int destY = -vSelection - origin.y;
                canvas.get().scroll(0, destY, 0, 0, timeline.getWidth(), timeline.getHeight(), false);
                origin.y = -vSelection;
            }
        });
        canvas.get().redraw();
        barCanvas.get().redraw();
    }

    @Override
    public int preferredSize() {
        return PANEL_PX;
    }

    @Override
    public int getPreferredPosition() {
        return SWT.TOP;
    }

    @Override
    public String getName() {
        return "Timeline";
    }

    @Override
    public void tick(TimeLapse timeLapse) {
    }

    @Override
    public void afterTick(TimeLapse timeLapse) {
        currentTime = timeLapse.getStartTime();
    }

    @Override
    public void render() {
        checkState(canvas.isPresent());
        if (canvas.get().isDisposed()) {
            return;
        }
        canvas.get().getDisplay().syncExec(new Runnable() {
            @Override
            public void run() {
                if (!canvas.get().isDisposed()) {
                    canvas.get().redraw();
                    barCanvas.get().redraw();
                }
            }
        });
    }

    static Image createNewTransparentImg(Display d, int w, int h) {
        final Color bg = d.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
        final PaletteData palette = new PaletteData(new RGB[] { bg.getRGB() });
        final ImageData sourceData = new ImageData(w, h, 1, palette);
        if (IS_MAC_OR_WINDOWS) {
            sourceData.transparentPixel = 0;
        }
        return new Image(d, sourceData);
    }

    /**
     * @return A new {@link Builder}.
     */
    public static Builder builder() {
        return new AutoValue_TimeLinePanel_Builder();
    }

    /**
     * Builder for {@link TimeLinePanel}.
     * @author Rinde van Lon
     */
    @AutoValue
    public abstract static class Builder extends AbstractModelBuilder<TimeLinePanel, Void> {

        Builder() {
            setDependencies(PDPModel.class);
        }

        @Override
        public TimeLinePanel build(DependencyProvider dependencyProvider) {
            return new TimeLinePanel(dependencyProvider.get(PDPModel.class));
        }
    }

    static class ParcelInfo {
        final long eventTime;
        final Parcel parcel;

        ParcelInfo(long time, Parcel p) {
            eventTime = time;
            parcel = p;
        }
    }

    static class TimelineBar {
        static final int LARGE_TICK_HEIGHT = 10;
        static final int SMALL_TICK_HEIGHT = 5;
        static final int LARGE_TICK_DIST = 40;
        static final int SMALL_TICK_DIST = 8;
        static final int TL_BAR_HEIGHT_PX = 20;
        static final int ADDITIONAL_WIDTH = 30;

        protected final Display display;
        protected Image contents;
        protected Font font;

        TimelineBar(Display d) {
            display = d;
            contents = createNewTransparentImg(display, WIDTH_PX, TL_BAR_HEIGHT_PX);
            font = new Font(display, "arial", FONT_SIZE, SWT.NORMAL);
            drawTimeline();
        }

        void update(int width) {
            if (contents.getBounds().width < width) {
                contents.dispose();
                contents = createNewTransparentImg(display, width + ADDITIONAL_WIDTH, TL_BAR_HEIGHT_PX);
                drawTimeline();
            }
        }

        final void drawTimeline() {
            final GC gc = new GC(contents);

            gc.setBackground(display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
            gc.fillRectangle(0, 0, contents.getBounds().width, contents.getBounds().height);

            gc.setAdvanced(true);
            gc.setTextAntialias(SWT.ON);

            for (int i = 0; i < contents.getBounds().width; i += SMALL_TICK_DIST) {
                final int height = i % LARGE_TICK_DIST == 0 ? LARGE_TICK_HEIGHT : SMALL_TICK_HEIGHT;
                if (i % LARGE_TICK_DIST == 0) {

                    final String time = FORMATTER.print(new Period(0L, TIME_PER_PIXEL * i));
                    gc.setFont(font);
                    final Point size = gc.textExtent(time);
                    gc.drawText(time, i - size.x / 2, 0, true);
                }
                gc.drawLine(i, TL_BAR_HEIGHT_PX - height, i, TL_BAR_HEIGHT_PX);
            }
            gc.dispose();
        }
    }

    static class Timeline {
        static final int ROW_HEIGHT = 15;
        static final int VERTICAL_DIST = 40;
        static final long HOUR = 60 * 60 * 1000;
        static final int START_HEIGHT = 100;
        static final int ADDITIONAL_HEIGHT_FACTOR = 10;

        static final int BAR_H = 12;
        static final int HALF_BAR_H = BAR_H / 2;
        static final int BAR_START_OFFSET_Y = 1;
        static final int BAR_END_OFFSET_Y = BAR_H + BAR_START_OFFSET_Y;
        static final int BAR_MIDDLE_OFFSET_Y = 8;

        final Display display;
        Optional<Image> contents;
        List<ParcelInfo> parcels;
        List<ParcelInfo> newParcels;

        Color lineColor;
        Color pickupColor;
        Color deliveryColor;
        Color gridColor;

        int width;
        private int height;

        Timeline(Display d) {
            display = d;
            parcels = newArrayList();
            newParcels = newArrayList();
            lineColor = d.getSystemColor(SWT.COLOR_WIDGET_BORDER);
            pickupColor = d.getSystemColor(SWT.COLOR_BLUE);
            deliveryColor = d.getSystemColor(SWT.COLOR_DARK_RED);
            gridColor = d.getSystemColor(SWT.COLOR_GRAY);
            contents = Optional.absent();
        }

        void ensureImg() {
            if (!contents.isPresent()) {
                contents = Optional.of(createNewTransparentImg(display, WIDTH_PX, START_HEIGHT));
                final GC gc = new GC(contents.get());
                drawVerticals(gc, WIDTH_PX, START_HEIGHT);
                gc.dispose();
            } else {
                final boolean wViolation = width > contents.get().getBounds().width;
                final boolean hViolation = height > contents.get().getBounds().height;
                if (wViolation || hViolation) {
                    final int newWidth = Math.max(width, contents.get().getBounds().width)
                            + (wViolation ? (int) (HOUR / TIME_PER_PIXEL) : 0);
                    final int newHeight = contents.get().getBounds().height
                            + (hViolation ? ADDITIONAL_HEIGHT_FACTOR * ROW_HEIGHT : 0);

                    final Image newContents = createNewTransparentImg(display, newWidth, newHeight);
                    // copy previous image to new image
                    final GC gc = new GC(newContents);
                    // draw vertical grid lines
                    drawVerticals(gc, newWidth, newHeight);
                    gc.drawImage(contents.get(), 0, 0);

                    gc.dispose();
                    contents.get().dispose();
                    contents = Optional.of(newContents);
                }
            }
        }

        void drawVerticals(GC gc, int w, int h) {
            for (int i = 0; i < w; i += VERTICAL_DIST) {
                gc.setForeground(gridColor);
                gc.drawLine(i, 0, i, h);
            }
        }

        void addParcel(final ParcelInfo p) {
            newParcels.add(p);

            width = Math.max(width, (int) (p.parcel.getDeliveryTimeWindow().end() / TIME_PER_PIXEL));
        }

        void update(int timeX) {
            if (display.isDisposed()) {
                return;
            }
            final int oldHeight = height;

            // make copy to avoid concurrency problems
            final List<ParcelInfo> copyNewParcels;
            synchronized (newParcels) {
                copyNewParcels = newArrayList(newParcels);
                newParcels.clear();
            }

            parcels.addAll(copyNewParcels);
            height = parcels.size() * ROW_HEIGHT;

            width = Math.max(width, timeX);
            ensureImg();
            for (int i = 0; i < copyNewParcels.size(); i++) {
                drawParcel(copyNewParcels.get(i), oldHeight + i * ROW_HEIGHT);
            }
        }

        void drawParcel(ParcelInfo p, int y) {
            final TimeWindow pi = p.parcel.getPickupTimeWindow();
            final TimeWindow de = p.parcel.getDeliveryTimeWindow();

            final int startX = (int) (p.eventTime / TIME_PER_PIXEL);
            final int startPickX = (int) (pi.begin() / TIME_PER_PIXEL);
            final int endPickX = (int) (pi.end() / TIME_PER_PIXEL);
            final int startDelX = (int) (de.begin() / TIME_PER_PIXEL);
            final int endDelX = (int) (de.end() / TIME_PER_PIXEL);

            final GC gc = new GC(contents.get());
            gc.setForeground(lineColor);
            gc.drawLine(startX, y + BAR_START_OFFSET_Y, startX, y + BAR_END_OFFSET_Y);
            gc.drawLine(startX, y + BAR_MIDDLE_OFFSET_Y, startPickX, y + BAR_MIDDLE_OFFSET_Y);

            gc.setBackground(pickupColor);
            gc.fillRectangle(startPickX, y + 2, Math.max(endPickX - startPickX, 1), HALF_BAR_H);
            gc.drawRectangle(startPickX, y + 2, Math.max(endPickX - startPickX, 1), HALF_BAR_H);

            gc.drawLine(endPickX, y + BAR_MIDDLE_OFFSET_Y, startDelX, y + BAR_MIDDLE_OFFSET_Y);

            gc.setBackground(deliveryColor);
            gc.fillRectangle(startDelX, y + BAR_MIDDLE_OFFSET_Y, Math.max(endDelX - startDelX, 1), HALF_BAR_H);
            gc.drawRectangle(startDelX, y + BAR_MIDDLE_OFFSET_Y, Math.max(endDelX - startDelX, 1), HALF_BAR_H);

            gc.dispose();
        }

        int getHeight() {
            return height;
        }

        int getWidth() {
            return width;
        }

        void dispose() {
            contents.get().dispose();
        }
    }
}