Java tutorial
/* * 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.graphics.Color; import de.bioviz.structures.Actuation; import de.bioviz.structures.Biochip; import de.bioviz.structures.BiochipField; import de.bioviz.structures.Dispenser; import de.bioviz.structures.Droplet; import de.bioviz.structures.FluidicConstraintViolation; import de.bioviz.structures.Mixer; import de.bioviz.structures.Net; import de.bioviz.structures.Point; import de.bioviz.structures.Rectangle; import de.bioviz.structures.Sink; import de.bioviz.util.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import static de.bioviz.ui.BDisplayOptions.Actuations; import static de.bioviz.ui.BDisplayOptions.ActuationSymbols; import static de.bioviz.ui.BDisplayOptions.Adjacency; import static de.bioviz.ui.BDisplayOptions.CellUsage; import static de.bioviz.ui.BDisplayOptions.CellUsageCount; import static de.bioviz.ui.BDisplayOptions.DetectorIcon; import static de.bioviz.ui.BDisplayOptions.HighlightAnnotatedFields; import static de.bioviz.ui.BDisplayOptions.InterferenceRegion; import static de.bioviz.ui.BDisplayOptions.LingeringInterferenceRegions; import static de.bioviz.ui.BDisplayOptions.MovementNeighbourhood; import static de.bioviz.ui.BDisplayOptions.NetColorOnFields; import static de.bioviz.ui.BDisplayOptions.Pins; import static de.bioviz.ui.BDisplayOptions.SourceTargetIDs; import static de.bioviz.ui.BDisplayOptions.SourceTargetIcons; /** * <p> The {@link DrawableField} class implements the element that draws its * corresponding {@link BiochipField} structure.</p> <p>The core element in this * case is the {@link BiochipField} variable called {@link DrawableField}'s * field variable, which links back to the original structural object. The * {@link DrawableField} then contains the additional information that is needed * to draw the field, which currently merely means some color information and * some drawing-related methods.</p> * * @author Jannis Stoppe */ public class DrawableField extends DrawableSprite { /** * The zoom level at which fields resort to drawing boxes instead of actual * structures. */ public static final float PIXELIZED_ZOOM_LEVEL = 8; /** * Used to log anything related to the {@link DrawableField} activities. */ private static Logger logger = LoggerFactory.getLogger(DrawableField.class); /** * The underlying structure that is drawn by this {@link DrawableField}'s * instance. */ protected BiochipField field; /** * The circuit this field is a part of. This again links to the drawable * version of the circuit, not the structure that represents the circuit * itself. */ private DrawableAssay parentAssay; /** * Creates an object that draws a given field for a biochip. * <p/> * <p>Notice that we separate the structure from the drawing, hence the * separation of Drawable-something vs structural classes. This class needs * the structural information what it's supposed to draw (given via the * field parameter) and the drawable parent circuit instance that it * belongs * to (via the parent parameter). We currently silently assume that the * structures are consistent (i.e. that the given field's parent circuit is * the one that is drawn via this instance's drawable parent) but do not * enforce any checks in this way, so don't break it.</p> * * @param field * the field that is supposed to be drawn by this instance * @param parent * this field's drawable parent */ public DrawableField(final BiochipField field, final DrawableAssay parent) { super(TextureE.GridMarker, parent.getParent()); this.setParentAssay(parent); this.setField(field); super.addLOD(PIXELIZED_ZOOM_LEVEL, TextureE.BlackPixel); this.setZ(DisplayValues.DEFAULT_FIELD_DEPTH); //adjacencyOverlay = new AdjacencyOverlay("AdjacencyMarker.png"); } /** * Retrieves information about this field: the color, the message being * displayed on top and the texture. All contained in a {@link * DisplayValues} instance. * * @return the current color, message and texture */ public DisplayValues getDisplayValues() { Pair<String, TextureE> msgTexture = getMsgTexture(); Color color = getColor(); return new DisplayValues(color, msgTexture.fst, msgTexture.snd); } /** * Retrieves this field's texture and the message being displayed on top. * * @return a {@link Pair} of message and texture. */ public Pair<String, TextureE> getMsgTexture() { String fieldHUDMsg = null; TextureE texture = TextureE.GridMarker; /* Right now, the first options that is tested and set to true determines the returned strings. This means that there might be a display of inconsistant data. For example source/target IDs may interfere with a detector ID. This is a real use case as a detector is a very valid routing target. There is an execption: the cell usage count overwrites any previous text. I really dislike this case by case hard coding :/ */ if (field.isPotentiallyBlocked()) { texture = TextureE.Blockage; } else if (field.getDetector() != null && getOption(DetectorIcon)) { texture = TextureE.Detector; } else if (field.getMagnet() != null) { texture = TextureE.Magnet; } else if (field.getHeater() != null) { texture = TextureE.Heater; } else if (field.isSource()) { if (getOption(SourceTargetIcons)) { texture = TextureE.Start; } if (getOption(SourceTargetIDs)) { ArrayList<Integer> sources = field.sourceIDs; fieldHUDMsg = sources.get(0).toString(); if (sources.size() > 1) { for (int i = 2; i < sources.size(); i++) { fieldHUDMsg += ", " + sources.get(i); } } } } else if (field.isTarget()) { if (getOption(SourceTargetIcons)) { texture = TextureE.Target; } if (getOption(SourceTargetIDs)) { ArrayList<Integer> targets = field.targetIDs; fieldHUDMsg = targets.get(0).toString(); if (targets.size() > 1) { for (int i = 1; i < targets.size(); i++) { fieldHUDMsg += ", " + targets.get(i); } } } } // note: this overwrites any previous message if (getOption(Pins) && field.pin != null) { fieldHUDMsg = Integer.toString(field.pin.pinID); } if (getOption(CellUsageCount)) { fieldHUDMsg = Integer.toString(field.getUsage()); } int t = getParentAssay().getCurrentTime(); if (getOption(ActuationSymbols)) { Actuation act = field.getActuation(t); switch (act) { case ON: fieldHUDMsg = "1"; break; case OFF: fieldHUDMsg = "0"; break; case DONTCARE: default: fieldHUDMsg = "X"; } } return Pair.mkPair(fieldHUDMsg, texture); } /** * Computes the cell coloring based on the usage. * * @param result * The color that is to be adjusted by this method. * @return 1 if cell usage was used, 0 otherwise */ private int cellUsageColoring(final de.bioviz.ui.Color result) { if (getOption(CellUsage)) { float scalingFactor = this.parentAssay.getData().getMaxUsage(); int usage = field.getUsage(); float color = usage / scalingFactor; result.add(new Color(color, color, color, 0)); return 1; } return 0; } /** * Decorates cells that are on the corners of a net bounding box. */ private void netColoring() { /** * The NetColorOnFields display option is a little special and thus * gets quite some amount of code here. * The idea is that we use the sprite's corner vertices and colorize * them separately *if* they are part of a net's edge. At first, these * colors are stored in the cornerColors array. When drawing, this * array is checked for existence and if it isn't null, each none-black * color *completely overrides* the given field color at this corner. */ if (getOption(NetColorOnFields)) { if (cornerColors == null) { cornerColors = new Color[4]; // one color for each corner } final int bottomleft = 0; final int topleft = 1; final int topright = 2; final int bottomright = 3; for (int i = 0; i < cornerColors.length; i++) { // Create non-null array contents cornerColors[i] = Color.BLACK.cpy(); } for (final Net net : this.getParentAssay().getData().getNetsOf(this.getField())) { de.bioviz.ui.Color netCol = net.getColor().cpy(); // Increase brightness for hovered nets if (this.parentAssay.getHoveredField() != null && this.getParentAssay().getData() .getNetsOf(this.getParentAssay().getHoveredField().field).contains(net)) { netCol.add(Colors.HOVER_NET_DIFF_COLOR); } Point top = new Point(field.x(), field.y() + 1); Point bottom = new Point(field.x(), field.y() - 1); Point left = new Point(field.x() - 1, field.y()); Point right = new Point(field.x() + 1, field.y()); Color color = netCol.buildGdxColor(); Biochip parent = getParentAssay().getData(); boolean fieldAtTop = parent.hasFieldAt(top); boolean fieldAtBottom = parent.hasFieldAt(bottom); boolean fieldAtLeft = parent.hasFieldAt(left); boolean fieldAtRight = parent.hasFieldAt(right); boolean containsTop = fieldAtTop && net.containsField(parent.getFieldAt(top)); boolean containsBottom = fieldAtBottom && net.containsField(parent.getFieldAt(bottom)); boolean containsLeft = fieldAtLeft && net.containsField(parent.getFieldAt(left)); boolean containsRight = fieldAtRight && net.containsField(parent.getFieldAt(right)); if (!fieldAtTop || !containsTop) { this.cornerColors[topleft].add(color); this.cornerColors[topright].add(color); } if (!fieldAtBottom || !containsBottom) { this.cornerColors[bottomleft].add(color); this.cornerColors[bottomright].add(color); } if (!fieldAtLeft || !containsLeft) { this.cornerColors[bottomleft].add(color); this.cornerColors[topleft].add(color); } if (!fieldAtRight || !containsRight) { this.cornerColors[topright].add(color); this.cornerColors[bottomright].add(color); } } for (int i = 0; i < cornerColors.length; i++) { if (cornerColors[i].equals(Color.BLACK)) { cornerColors[i] = super.getColor(); } } } else { cornerColors = null; } } /** * Calculates the current color based on the parent circuit's * displayOptions. * * @return the field's color. */ @Override public Color getColor() { /** * This value stores the amount of colors being overlaid in the process * of computing the color. This is currently required to calculate the * average value of all colors at the end of the process (e.g. if three * different colors are being added, the final result needs to be * divided by three). */ int colorOverlayCount = 0; /* We need to create a copy of the FIELD_EMPTY_COLOR as that value is final and thus can not be modified. If that value is unchangeable, the cells all stay white */ de.bioviz.ui.Color result = new de.bioviz.ui.Color(Color.BLACK); if (getField().isBlocked(getParentAssay().getCurrentTime())) { result.add(Colors.BLOCKED_COLOR); colorOverlayCount++; } netColoring(); colorOverlayCount += cellUsageColoring(result); colorOverlayCount += inteferenceRegionColoring(result); colorOverlayCount += reachableRegionColoring(result); /** * Here we highlight cells based on their actuation value */ int t = getParentAssay().getCurrentTime(); if (getOption(Actuations)) { Actuation act = field.getActuation(t); switch (act) { case ON: result.add(Colors.ACTUATION_ON_COLOR); break; case OFF: result.add(Colors.ACTUATION_OFF_COLOR); break; case DONTCARE: default: result.add(Colors.ACTUATION_DONTCARE_COLOR); } ++colorOverlayCount; } if (colorOverlayCount == 0) { colorOverlayCount += typeColoring(result, t); } if (getOption(Adjacency)) { final Stream<FluidicConstraintViolation> violations = getParentAssay().getData() .getAdjacentActivations().stream(); if (violations.anyMatch(v -> v.containsField(this.field))) { result.add(Colors.ADJACENT_ACTIVATION_COLOR); } } if (colorOverlayCount > 0) { result.mul(1f / ((float) colorOverlayCount)); result.clamp(); } else { result = new de.bioviz.ui.Color(Colors.FIELD_COLOR); } if (this.isHovered()) { result.add(Colors.HOVER_DIFF_COLOR); } if (getOption(HighlightAnnotatedFields) && field.hasAnnotations()) { result = new de.bioviz.ui.Color(Color.VIOLET); } return result.buildGdxColor().cpy(); } /** * Computes the color based on the type of the field. * * @param result * The resulting color. * @param timeStep * The current time step. * @return The amount of new color overlays. */ private int typeColoring(final de.bioviz.ui.Color result, final int timeStep) { int colorOverlayCount = 0; if (field instanceof Sink) { result.add(Colors.SINK_COLOR); colorOverlayCount++; } else if (field instanceof Dispenser) { result.add(Colors.SOURCE_COLOR); colorOverlayCount++; } else { result.add(Colors.FIELD_COLOR); colorOverlayCount++; } for (final Mixer m : field.mixers) { if (m.timing.inRange(timeStep)) { result.add(Colors.MIXER_COLOR); } } return colorOverlayCount; } /** * Colors the field based on reachability by droplets. * * @param result Return parameter storing the color that is computed after * applying the reachability check. * @return 1 if the field can be reached within one time step by any * droplet, 0 otherwise. */ private int reachableRegionColoring(final de.bioviz.ui.Color result) { int colorOverlayCount = 0; if (getOption(MovementNeighbourhood)) { int t = getParentAssay().getCurrentTime(); final Set<Droplet> dropsSet = getParentAssay().getData().getDroplets(); boolean fieldIsReachable = dropsSet.stream().map(d -> d.getPositionAt(t)).filter(Objects::nonNull) .anyMatch(p -> p.reachable(field.pos)); if (fieldIsReachable) { result.add(Colors.REACHABLE_FIELD_COLOR); colorOverlayCount = 1; } } return colorOverlayCount; } /** * Colors based on the interference region. * * @param result * The color that results from this method call. * @return The amount of color overlays produced by this method. */ private int inteferenceRegionColoring(final de.bioviz.ui.Color result) { int colorOverlayCount = 0; boolean isBlocked = getField().isBlocked(getParentAssay().getCurrentTime()); /** Colours the interference region **/ if (getOption(InterferenceRegion)) { int amountOfInterferenceRegions = 0; final Set<Droplet> dropsSet = getParentAssay().getData().getDroplets(); ArrayList<Droplet> drops = dropsSet.stream().filter(d -> isPartOfInterferenceRegion(d)) .collect(Collectors.toCollection(ArrayList<Droplet>::new)); for (int i = 0; i < drops.size(); ++i) { boolean interferenceViolation = false; for (int j = i + 1; j < drops.size(); j++) { final Droplet drop1 = drops.get(i); final Droplet drop2 = drops.get(j); boolean sameNet = getParentAssay().getData().sameNet(drop1, drop2); if (!sameNet && !isBlocked) { result.add(Colors.INTERFERENCE_REGION_OVERLAP_COLOR); ++colorOverlayCount; interferenceViolation = true; } } /* We only increase the amount of interference regions if no violation took place. This makes sense as a violation is handled differently. */ if (!interferenceViolation) { ++amountOfInterferenceRegions; } } if (amountOfInterferenceRegions > 0 && !isBlocked) { float scale = (float) Math.sqrt(amountOfInterferenceRegions); Color c = new Color(Colors.INTERFERENCE_REGION_COLOR); result.add(c.mul(scale)); ++colorOverlayCount; } } return colorOverlayCount; } @Override public void draw() { DisplayValues vals = getDisplayValues(); displayText(vals.getMsg()); setColor(vals.getColor()); DrawableAssay circ = getParentAssay(); float xCoord = circ.xCoordOnScreen(getField().x()); float yCoord = circ.yCoordOnScreen(getField().y()); this.setX(xCoord); this.setY(yCoord); this.setScaleX(circ.getSmoothScale()); this.setScaleY(circ.getSmoothScale()); // this call is actually necessary to draw any textures at all! this.addLOD(Float.MAX_VALUE, vals.getTexture()); super.draw(); // show the first annotation for this field if (isHovered() && !field.areaAnnotations.isEmpty()) { displayText(field.areaAnnotations.get(0).getAnnotation()); } // TODO why is drawing of lines in any way tied to the actual fields?! } /** * Calculates whether or not this field is part of a droplet's interference * region. * * @param d * the droplet to calculate it for * @return whether or not this field is part of its interference region */ private boolean isPartOfInterferenceRegion(final Droplet d) { Rectangle currPos = d.getPositionAt(getParentAssay().getCurrentTime()); Rectangle prevPos = d.getPositionAt(getParentAssay().getCurrentTime() - 1); boolean currAdj = currPos != null && currPos.adjacent(field.pos); if (getOption(LingeringInterferenceRegions)) { boolean prevAdj = prevPos != null && prevPos.adjacent(field.pos); return currAdj || prevAdj; } else { return currAdj; } } /** * Retrieves the *structural* field that is drawn by this {@link * DrawableField}. * * @return the field that is drawn by this {@link DrawableField} */ public BiochipField getField() { return field; } /** * Sets the field that is drawn by this {@link DrawableField}. This * shouldn't really be used at any point after the {@link DrawableAssay} * has been fully initialized. * * @param field * the field that should be drawn by this {@link DrawableField} */ public void setField(final BiochipField field) { this.field = field; } /** * Retrieves the parent circuit of this field. * * @return the circuit that contains this field */ public DrawableAssay getParentAssay() { return parentAssay; } /** * Sets the parent circuit of this field. This shouldn't really be used * after the whole circuit has been initialized. * * @param parentAssay * the circuit that contains this field. */ public void setParentAssay(final DrawableAssay parentAssay) { this.parentAssay = parentAssay; } /** * Convenience method for checking options. * * @param optn * Option to check * @return true if option is true */ protected boolean getOption(final BDisplayOptions optn) { return getParentAssay().getDisplayOptions().getOption(optn); } }