de.bioviz.ui.DrawableAssay.java Source code

Java tutorial

Introduction

Here is the source code for de.bioviz.ui.DrawableAssay.java

Source

/*
 * BioViz, a visualization tool for digital microfluidic biochips (DMFB).
 *
 * Copyright (c) 2017 Oliver Keszocze, Jannis Stoppe, Maximilian Luenert
 *
 * This file is part of BioViz.
 *
 * BioViz is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 2 of the License, or (at your option)
 * any later version.
 *
 * BioViz is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * See the GNU General Public License for more details. You should have
 * received a copy of the GNU
 * General Public License along with BioViz.
 * If not, see <http://www.gnu.org/licenses/>.
 */

package de.bioviz.ui;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.Rectangle;
import de.bioviz.structures.Biochip;
import de.bioviz.structures.Dispenser;
import de.bioviz.structures.Droplet;
import de.bioviz.structures.Net;
import de.bioviz.structures.Point;
import de.bioviz.structures.Sink;
import de.bioviz.util.Quadruple;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * The DrawableAssay class collects the data of an assay that is executed on
 * a biochip.
 * <p>
 * It also provides the means to draw it using libgdx.
 *
 * @author Jannis Stoppe
 */
public class DrawableAssay implements Drawable {

    /**
     * The default value for the scalingDelay field.
     */
    private static final float DEFAULT_SCALING_DELAY = 4f;

    /**
     * The logger for this instance.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(DrawableAssay.class);

    /**
     * This represents the actual biochip structure that is drawn by this
     * DrawableAssay.
     */
    private Biochip data;

    /**
     * The current UI scaling factor.
     */
    private float scale = 1;

    /**
     * The current UI x offset (used for camera movement etc.).
     */
    private float offsetX = 0;

    /**
     * The current UI scaling factor.
     */
    private float scaleY = 1;

    /**
     * The current UI y offset (used for camera movement etc.).
     */
    private float offsetY = 0;

    /**
     * The "smooth" scaling factor. Unlike the scale values, this does not
     * represent the scale value that the user has last set but the one that is
     * actually drawn. This is used for a smooth camera zoom, e.g. the user
     * zooms in, which sets the scale value to a specific value and the
     * smoothScale value is then over the duration of several frames slowly
     * adjusted to the scale value in order to have a smoother cam movement.
     */
    private float smoothScale = 1;

    /**
     * The "smooth" offset. Unlike the offset values, this does not represent
     * the offset value that the user has last set but the one that is actually
     * drawn. This is used for a smooth camera movement, e.g. the user pans,
     * which sets the offset value to a specific value and the smoothOffset
     * value is then over the duration of several frames slowly adjusted to the
     * offset value in order to have a smoother cam movement.
     */
    private float smoothOffsetX = 0;

    /**
     * The "smooth" offset. Unlike the offset values, this does not represent
     * the offset value that the user has last set but the one that is actually
     * drawn. This is used for a smooth camera movement, e.g. the user pans,
     * which sets the offset value to a specific value and the smoothOffset
     * value is then over the duration of several frames slowly adjusted to the
     * offset value in order to have a smoother cam movement.
     */
    private float smoothOffsetY = 0;

    /**
     * The delay that is applied to the smooth cam movement. The value is
     * altered every frame by a value of (offset - smoothOffset) / scalingDelay
     * (respectively scale), so <b>increasing</b> this value results in
     * <b>slower movement</b> of the camera.
     */
    private float scalingDelay = DEFAULT_SCALING_DELAY;

    /**
     * The timestep of the data field that is currently being drawn.
     */
    private int currentTime = 1;

    /**
     * A flag that specifies whether or not the display automatically advances
     * in time.
     */
    private boolean autoAdvance = false;

    /**
     * The speed at which the dispaly advances through time <b>if</b> the
     * autoAdvance field is set.
     */
    private float autoDelay = 2f;

    /**
     * The last timestep at which time was automatically advanced.
     */
    private long lastAutoStepAt = new Date().getTime();

    /**
     * This contains instances that need to be notified as soon as the
     * currentTime value is altered.
     */
    private ArrayList<BioVizEvent> timeChangedListeners = new ArrayList<>();

    /**
     * The fields that are present on this circuit.
     */
    private ArrayList<DrawableField> fields = new ArrayList<>();

    /**
     * The droplets that are present on this circuit.
     */
    private ArrayList<DrawableDroplet> droplets = new ArrayList<>();

    /**
     * The droplets that are present on this circuit but should not be drawn at
     * this moment.
     */
    private ArrayList<DrawableDroplet> hiddenDroplets = new ArrayList<>();

    /**
     * The nets that are contained within this assay.
     * <p>
     * (captain obvious to teh rescue!)
     */
    private List<DrawableNet> nets = new ArrayList<>();

    /**
     * The current displayOptions that determine drawing parameters.
     */
    private DisplayOptions displayOptions = new DisplayOptions();

    /**
     * The visualization this circuit is drawn in.
     */
    private BioViz parent;

    /**
     * This is a helper member that is incremented as soon as the time of the
     * circuit advances beyond the maximum time. This value can then be
     * compared to an arbitrary value and the whole circuit be reset to time 0
     * after a certain grace period of being already past its final timestamp.
     */
    private int autoloopOvertimeCounter = 0;

    /**
     * This member stores the grace period after which the auto loop starts
     * from
     * the beginning.
     *
     * @see autoloopOvertimeCounter
     */
    private int autoloopOvertimeGracePeriod = 5;

    /**
     * The field that is currently hovered by the mouse. May be null, so be
     * careful.
     */
    private DrawableField hoveredField = null;

    /**
     * The desired length of the displayed routes.
     */
    private int displayRouteLength = 0;

    /**
     * Creates a drawable entity based on the data given.
     *
     * @param toDraw
     *       the data to draw
     * @param parent
     *       The visualization this circuit belongs to.
     */
    DrawableAssay(final Biochip toDraw, final BioViz parent) {
        LOGGER.debug("Creating new drawable chip based on " + toDraw);
        this.setData(toDraw);
        this.setParent(parent);
        this.initializeDrawables();
        this.getDisplayOptions().addOptionChangedEvent(e -> {
            if (e.equals(BDisplayOptions.CellUsage) || e.equals(BDisplayOptions.CellUsageCount)) {
                boolean doIt = getDisplayOptions().getOption(e);
                if (doIt) {
                    getData().computeCellUsage();
                }
            }
        });
        LOGGER.debug("New DrawableAssay created successfully.");
    }

    /**
     * Skip to the previous timestep.
     */
    public void prevStep() {
        setAutoAdvance(false);
        setCurrentTime(getCurrentTime() - 1);
    }

    /**
     * Advance to the next timestep.
     */
    public void nextStep() {
        setAutoAdvance(false);
        setCurrentTime(getCurrentTime() + 1);
    }

    /**
     * Toggle (ie. enable if disabled or vice versa) the auto advance mode that
     * automatically advances through the timesteps.
     */
    public void toggleAutoAdvance() {
        this.setAutoAdvance(!(this.isAutoAdvance()));
    }

    /**
     * Jump to a specific timestep.
     *
     * @param timeStep
     *       the timestep to jump to.
     */
    public void setCurrentTime(final int timeStep) {
        if (getParent() != null) {
            if (timeStep >= 1 && timeStep <= getData().getMaxT()) {
                currentTime = timeStep;
                getParent().callTimeChangedListeners();
            }
        } else {
            throw new RuntimeException("assay parent is null");
        }
    }

    /**
     * Sets the length of the routes to be displayed.
     *
     * The value is capped at 0 and the maximal length of the routes present
     * in the assay.
     *
     * @param length Length of the displayed routes.
     */
    public void setDisplayRouteLength(final int length) {
        if (length >= 0 && length <= getData().getMaxRouteLength()) {
            displayRouteLength = length;
        }
    }

    /**
     * Returns how long routes are to be drawn.
     * @return The max length routes have when being displayed.
     */
    public int getDisplayRouteLength() {
        return displayRouteLength;
    }

    /**
     * Initializes the drawables according to the circuit stored in the data
     * field.
     */
    private void initializeDrawables() {
        // clear remaining old data first, if any
        this.getFields().clear();
        this.getDroplets().clear();

        LOGGER.debug("Initializing drawables: {} fields, {} droplets", getData().getAllCoordinates().size(),
                getData().getDroplets().size());

        //setup fields
        getData().getAllFields().forEach(fld -> {
            if (fld instanceof Sink) {
                fields.add(new DrawableSink((Sink) fld, this));
            } else if (fld instanceof Dispenser) {
                fields.add(new DrawableDispenser((Dispenser) fld, this));
            } else {
                DrawableField f = new DrawableField(fld, this);
                fields.add(f);
            }

        });

        LOGGER.debug("Fields set up.");

        //setup droplets
        for (final Droplet d : getData().getDroplets()) {
            DrawableDroplet dd = new DrawableDroplet(d, this);
            this.getDroplets().add(dd);
        }

        LOGGER.debug("Droplets set up.");

        //setup nets
        for (final Net n : getData().getNets()) {
            DrawableNet nn = new DrawableNet(n, this);
            this.getNets().add(nn);
        }

        LOGGER.debug("Nets set up.");

        LOGGER.debug("Drawable initialization successfully done.");
    }

    @Override
    public void draw() {

        setSmoothScale(getSmoothScale() + (getScaleX() - getSmoothScale()) / scalingDelay);
        setSmoothOffsetX(getSmoothOffsetX() + (getOffsetX() - getSmoothOffsetX()) / scalingDelay);
        setSmoothOffsetY(getSmoothOffsetY() + (getOffsetY() - getSmoothOffsetY()) / scalingDelay);

        if (getDisplayOptions().getOption(BDisplayOptions.Coordinates)) {
            displayCoordinates();
        } else {
            removeDisplayedCoordinates();
        }

        final long autoDelayScaling = 1000;
        final long scaledAutoDelay = (long) ((this.getAutoDelay()) * autoDelayScaling);
        if (isAutoAdvance()) {
            long current = new Date().getTime();
            if (lastAutoStepAt + scaledAutoDelay < current) {
                lastAutoStepAt = current;

                LOGGER.trace("data.getMaxT: {}\tcurrentTime: {}", getData().getMaxT(), getCurrentTime());
                setCurrentTime(getCurrentTime() + 1);
                if (getCurrentTime() >= getData().getMaxT()
                        && this.getDisplayOptions().getOption(BDisplayOptions.LoopAutoplay)) {
                    ++autoloopOvertimeCounter;
                    if (autoloopOvertimeCounter > autoloopOvertimeGracePeriod) {
                        setCurrentTime(1);
                        autoloopOvertimeCounter = 0;
                    }
                }
            }
        }

        for (final DrawableField f : this.getFields()) {
            if (f.isHovered()) {
                this.hoveredField = f;
            }
            f.draw();
        }

        for (final DrawableDroplet d : this.getDroplets()) {
            d.draw();
        }

        for (final DrawableNet n : this.getNets()) {
            n.draw();
        }

    }

    /**
     * Draws the coordinates of the grid on top of and to the left of the grid.
     * This in fact uses the message center to display the numbers, so the
     * actual drawing will be done after the rest has been drawn.
     */
    private void displayCoordinates() {

        Quadruple<Integer, Integer, Integer, Integer> minMaxVals = minMaxXY();

        int minX = minMaxVals.fst;
        int minY = minMaxVals.snd;
        int maxX = minMaxVals.thd;
        int maxY = minMaxVals.fth;

        final int offset = 32;

        float topYCoord = Gdx.graphics.getHeight() / 2f - offset;
        if (topYCoord > this.yCoordOnScreen(maxY + 1)) {
            topYCoord = this.yCoordOnScreen(maxY + 1);
        }
        float leftXCoord = -Gdx.graphics.getWidth() / 2f + offset;
        if (leftXCoord < this.xCoordOnScreen(minX - 1)) {
            leftXCoord = this.xCoordOnScreen(minX - 1);
        }

        // Defines when numbers should start fading and be completely hidden
        final float startFadingAtScale = 32f;
        final float endFadingAtScale = 24f;

        Color col = Color.WHITE.cpy();
        if (this.getSmoothScale() < startFadingAtScale) {
            if (this.getSmoothScale() > endFadingAtScale) {
                float alpha = 1f
                        - ((startFadingAtScale - getSmoothScale()) / (startFadingAtScale - endFadingAtScale));
                col.a = alpha;
            } else {
                col.a = 0;
            }
        }

        // indeed draw, top first, then left
        for (int i = minX; i < maxX + 1; i++) {
            this.getParent().messageCenter.addHUDMessage(this.hashCode() + i, // unique ID for each message
                    Integer.toString(i).trim(), // message
                    this.xCoordOnScreen(i), // x
                    topYCoord, // y
                    col // message color, used for fading
            );
        }

        for (int i = minY; i < maxY + 1; i++) {
            this.getParent().messageCenter.addHUDMessage(this.hashCode() + maxX + Math.abs(minY) + 1 + i,
                    // unique ID for each message, starting after the previous
                    // ids

                    Integer.toString(i).trim(), // message
                    leftXCoord, // x
                    this.yCoordOnScreen(i), // y
                    col // message color, used for fading
            );
        }
    }

    /**
     * Calculates the current dimensions.
     *
     * @return Quadruple (minX,minY,maxX,maxY)
     */
    private Quadruple<Integer, Integer, Integer, Integer> minMaxXY() {
        int minX = Integer.MAX_VALUE;
        int minY = Integer.MAX_VALUE;
        int maxX = Integer.MIN_VALUE;
        int maxY = Integer.MIN_VALUE;

        for (final DrawableField f : this.getFields()) {
            if (minX > f.getField().x()) {
                minX = f.getField().x();
            }
            if (minY > f.getField().y()) {
                minY = f.getField().y();
            }
            if (maxX < f.getField().x()) {
                maxX = f.getField().x();
            }
            if (maxY < f.getField().y()) {
                maxY = f.getField().y();
            }
        }
        return new Quadruple<>(minX, minY, maxX, maxY);
    }

    private void removeDisplayedCoordinates() {
        Quadruple<Integer, Integer, Integer, Integer> minMaxVals = minMaxXY();

        int minX = minMaxVals.fst;
        int minY = minMaxVals.snd;
        int maxX = minMaxVals.thd;
        int maxY = minMaxVals.fth;

        // remove all HUD messages
        for (int i = minX; i < maxX + Math.abs(minY) + 2 + maxY; i++) {
            this.getParent().messageCenter.removeHUDMessage(this.hashCode() + i);
        }
    }

    /**
     * Calculates the x coordinate of a given cell.
     *
     * @param i
     *       the cell index
     * @return the x coordinate on screen
     */
    protected float xCoordOnScreen(final int i) {
        return xCoordOnScreen((float) i);
    }

    /**
     * Calculates the x coordinate of a given value. Keep in mind that this is
     * still in cell-space, so a value of 0 would be at the center of the
     * chip's
     * first cell.
     *
     * @param i
     *       the value to translate
     * @return the x coordinate on screen
     */
    float xCoordOnScreen(final float i) {
        float xCoord = i;
        xCoord += getSmoothOffsetX();
        xCoord *= getSmoothScale();
        return xCoord;
    }

    float yCoordOnScreen(final int i) {
        return yCoordOnScreen((float) i);
    }

    float yCoordOnScreen(final float i) {
        float yCoord = i;
        yCoord += getSmoothOffsetY();
        yCoord *= getSmoothScale();
        return yCoord;
    }

    protected float yCoordInCells(final float i) {
        float yCoord = i;
        yCoord /= getSmoothScale();
        yCoord -= getSmoothOffsetY();
        return yCoord;
    }

    protected float xCoordInCells(final float i) {
        float xCoord = i;
        xCoord /= getSmoothScale();
        xCoord -= getSmoothOffsetX();
        return xCoord;
    }

    /**
     * If the two scaling factors aren't equal, this sets the larger scaling
     * factor to the smaller one in order to display square elements on screen.
     */
    public void shrinkToSquareAlignment() {
        if (getScaleY() < getScaleX()) {
            setScaleX(getScaleY());
        } else {
            setScaleY(getScaleX());
        }
    }

    /**
     * Retrieves the current x scaling factor.
     *
     * @return The Current x scaling factor.
     */
    public float getScaleX() {
        return scale;
    }

    /**
     * sets the current x scaling factor Keep in mind that the value used for
     * actually drawing the circuit is successively approaching the given value
     * for a smooth camera movement. Use setScaleImmediately if the viewport is
     * supposed to skip those inbetween steps.
     *
     * @param scaleX
     *       The new value for the x scaling value.
     */
    public void setScaleX(final float scaleX) {
        this.scale = scaleX;
    }

    /**
     * Retrieves the current x scaling factor that is used for the smooth
     * animated camera.
     *
     * @return The current x scaling factor.
     */
    public float getSmoothScaleX() {
        return getSmoothScale();
    }

    /**
     * Retrieves the current y scaling factor.
     *
     * @return The scaling factor for the y axis.
     */
    public float getScaleY() {
        return scaleY;
    }

    /**
     * Sets the current y scaling factor. Keep in mind that the value used for
     * actually drawing the circuit is successively approaching the given value
     * for a smooth camera movement. Use setScaleImmediately if the viewport is
     * supposed to skip those in between steps.
     *
     * @param scaleY
     *       The new scaling factor for the y axis.
     */
    public void setScaleY(final float scaleY) {
        this.scaleY = scaleY;
    }

    /**
     * Calculates the screen bounds.
     *
     * @return the screen bounds
     */
    public Rectangle getViewBounds() {
        Rectangle result = new Rectangle();

        float centerX = -getOffsetX();
        float width = Gdx.graphics.getWidth() * (1f / scale);
        float centerY = getOffsetY();
        float height = Gdx.graphics.getHeight() * (1f / scaleY);
        result.set(centerX - (width / 2f), centerY - (height / 2f), width, height);
        return result;
    }

    /**
     * Sets the screen bounds.
     *
     * @param bounds
     *       the area the viewport is supposed to show.
     */
    public void setViewBounds(final Rectangle bounds) {
        float targetHeight = Gdx.graphics.getHeight() / bounds.height;
        float targetWidth = Gdx.graphics.getWidth() / bounds.width;
        float targetOffsetX = bounds.x + (bounds.width / 2f);
        float targetOffsetY = bounds.y + (bounds.height / 2f);

        setScaleX(targetWidth);
        setScaleY(targetHeight);
        this.setOffsetX(-targetOffsetX);
        this.setOffsetY(targetOffsetY);
    }

    /**
     * Resets the zoom to 1 px per element.
     */
    public void zoomTo1Px() {
        this.scale = 1;
        this.scaleY = 1;
    }

    /**
     * Resets the zoom so that the whole chip is shown.
     */
    public void zoomExtents() {
        Point max = this.getData().getMaxCoord();
        Point min = this.getData().getMinCoord();
        LOGGER.debug("Auto zoom around " + min + " <--/--> " + max);

        float x = 1f / (max.fst - min.fst + 2);
        float y = 1f / (max.snd - min.snd + 2);
        float xFactor = Gdx.graphics.getWidth();
        float yFactor = Gdx.graphics.getHeight();
        float maxScale = Math.min(x * xFactor, y * yFactor);
        this.scale = maxScale;
        this.scaleY = maxScale;
        this.setOffsetX((max.fst) / -2f + min.fst / -2f);
        this.setOffsetY((max.snd) / -2f + min.snd / -2f);

        LOGGER.debug("Offset now at " + this.getOffsetX() + "/" + this.getOffsetY());
    }

    /**
     * Resets the zoom so that the whole circuit is shown without smoothly
     * zooming to those settings.
     */
    public void zoomExtentsImmediately() {
        zoomExtents();
        this.setSmoothScale(scale);
    }

    /**
     * Sets the zoom to the given values without smoothly approaching those
     * target values (instead sets them immediately).
     *
     * @param newScale
     *       The new immediate scale.
     */
    public void setScaleImmediately(final float newScale) {
        this.scale = newScale;
        this.setSmoothScale(newScale);
    }

    public void addTimeChangedListener(final BioVizEvent listener) {
        timeChangedListeners.add(listener);
    }

    /**
     * Retrieves the field that is currently being hovered.
     *
     * @return the currently hovered field.
     */
    public DrawableField getHoveredField() {
        return this.hoveredField;
    }

    public Biochip getData() {
        return data;
    }

    public void setData(final Biochip data) {
        this.data = data;
    }

    float getOffsetX() {
        return offsetX;
    }

    void setOffsetX(final float offsetX) {
        this.offsetX = offsetX;
    }

    float getOffsetY() {
        return offsetY;
    }

    void setOffsetY(final float offsetY) {
        this.offsetY = offsetY;
    }

    float getSmoothScale() {
        return smoothScale;
    }

    protected float getSmoothOffsetX() {
        return smoothOffsetX;
    }

    protected void setSmoothOffsetX(final float smoothOffsetX) {
        this.smoothOffsetX = smoothOffsetX;
    }

    protected float getSmoothOffsetY() {
        return smoothOffsetY;
    }

    protected void setSmoothOffsetY(final float smoothOffsetY) {
        this.smoothOffsetY = smoothOffsetY;
    }

    public int getCurrentTime() {
        return currentTime;
    }

    public boolean isAutoAdvance() {
        return autoAdvance;
    }

    public void setAutoAdvance(final boolean autoAdvance) {
        this.autoAdvance = autoAdvance;
    }

    public float getAutoDelay() {
        return autoDelay;
    }

    public void setAutoDelay(final float autoDelay) {
        this.autoDelay = autoDelay;
    }

    public List<DrawableField> getFields() {
        return fields;
    }

    public void setFields(final ArrayList<DrawableField> fields) {
        this.fields = fields;
    }

    public List<DrawableDroplet> getDroplets() {
        return droplets;
    }

    public void setDroplets(final ArrayList<DrawableDroplet> droplets) {
        this.droplets = droplets;
    }

    /**
     * Checks whether a certain droplet is hidden.
     * <p>
     * 'Hidden' means that the droplet is not present on the field as such but
     * displayed on the top right corner.
     *
     * @param drop
     *       Droplet to test
     * @return true if the droplet is hidden, false otherwise.
     */
    public boolean isHidden(final DrawableDroplet drop) {
        return hiddenDroplets.contains(drop);
    }

    /**
     * Hides a droplet.
     *
     * @param drop
     *       Droplet to hide
     */
    public void hideDroplet(final DrawableDroplet drop) {
        hiddenDroplets.add(drop);
    }

    /**
     * Unhides a droplet.
     *
     * @param drop
     *       The droplet to unhide.
     */
    public void unHideDroplet(final DrawableDroplet drop) {
        hiddenDroplets.remove(drop);
    }

    public List<DrawableDroplet> getHiddenDroplets() {
        return hiddenDroplets;
    }

    public void setHiddenDroplets(final ArrayList<DrawableDroplet> hiddenDroplets) {
        this.hiddenDroplets = hiddenDroplets;
    }

    public List<DrawableNet> getNets() {
        return nets;
    }

    public void setNets(final List<DrawableNet> nets) {
        this.nets.clear();
        this.nets.addAll(nets);
    }

    public DisplayOptions getDisplayOptions() {
        return displayOptions;
    }

    public void setDisplayOptions(final DisplayOptions displayOptions) {
        this.displayOptions = displayOptions;
    }

    public BioViz getParent() {
        return parent;
    }

    public void setParent(final BioViz parent) {
        this.parent = parent;
    }

    private void setSmoothScale(final float smoothScale) {
        this.smoothScale = smoothScale;
    }
}