All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.rinde.rinsim.pdptw.common.TimeLinePanel Maven / Gradle / Ivy

There is a newer version: 4.4.6
Show newest version
/*
 * 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;
  Optional 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 {

    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 contents;
    List parcels;
    List 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 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();
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy