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.parser; import de.bioviz.parser.generated.Bio; import de.bioviz.parser.generated.Bio.BioContext; import de.bioviz.parser.generated.Bio.BlockageContext; import de.bioviz.parser.generated.Bio.CellActuationContext; import de.bioviz.parser.generated.Bio.DispenserContext; import de.bioviz.parser.generated.Bio.DropletIDContext; import de.bioviz.parser.generated.Bio.FluidIDContext; import de.bioviz.parser.generated.Bio.FluiddefContext; import de.bioviz.parser.generated.Bio.GridblockContext; import de.bioviz.parser.generated.Bio.MixerIDContext; import de.bioviz.parser.generated.Bio.PinActuationContext; import de.bioviz.parser.generated.Bio.PinIDContext; import de.bioviz.parser.generated.Bio.PositionContext; import de.bioviz.parser.generated.Bio.RouteContext; import de.bioviz.parser.generated.Bio.SourceContext; import de.bioviz.parser.generated.Bio.TimeConstraintContext; import de.bioviz.parser.generated.Bio.TimeRangeContext; import de.bioviz.parser.generated.Bio.TimingContext; import de.bioviz.parser.generated.BioBaseListener; import de.bioviz.structures.ActuationVector; import de.bioviz.structures.AreaAnnotation; import de.bioviz.structures.Biochip; import de.bioviz.structures.BiochipField; import de.bioviz.structures.Detector; import de.bioviz.structures.Direction; import de.bioviz.structures.Dispenser; import de.bioviz.structures.Droplet; import de.bioviz.structures.FluidicConstraintViolation; import de.bioviz.structures.Heater; import de.bioviz.structures.Magnet; import de.bioviz.structures.Mixer; import de.bioviz.structures.Net; import de.bioviz.structures.Pin; import de.bioviz.structures.Point; import de.bioviz.structures.Range; import de.bioviz.structures.Rectangle; import de.bioviz.structures.Sink; import de.bioviz.structures.Source; import de.bioviz.util.Pair; import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.misc.NotNull; import org.antlr.v4.runtime.misc.Nullable; import org.antlr.v4.runtime.tree.TerminalNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Optional; import java.util.Set; /** * This class implements a Listener for the BioParser. * * @author Oliver Keszocze?, Jannis Stoppe?, Maximilian Luenert */ class BioParserListener extends BioBaseListener { /** * The logger instance. */ private static Logger logger = LoggerFactory.getLogger(BioParserListener.class); /** * Stores all the parsed rectangles used to define the cells of the chip. */ private ArrayList<Rectangle> rectangles = new ArrayList<>(); /** * Stores all the parsed droplets. */ private ArrayList<Droplet> droplets = new ArrayList<>(); /** * Stores the maxX coordinate. */ private int maxX = 0; /** * Stores the maxY coordinate. */ private int maxY = 0; /** * Stores the number of grids that were defined in the input file. */ private int nGrids = 0; /** * Stores all parsed fluidTypes. */ private HashMap<Integer, String> fluidTypes = new HashMap<>(); /** * Stores alls parsed nets. */ private ArrayList<Net> nets = new ArrayList<>(); /** * Stores a connection from dropletIds to FluidTypes. */ private HashMap<Integer, Integer> dropletIDsToFluidTypes = new HashMap<>(); /** * Stores all parsed sinks. */ private ArrayList<SimpleExternalResource> sinks = new ArrayList<>(); /** * Stores all parsed dispensers. */ private ArrayList<SimpleExternalResource> dispensers = new ArrayList<>(); /** * Stores all parsed blockages. */ private ArrayList<Pair<Rectangle, Range>> blockages = new ArrayList<>(); /** * Stores all parsed detectors. */ private ArrayList<Detector> detectors = new ArrayList<>(); /** * Stores all parsed heaters. */ private ArrayList<Heater> heaters = new ArrayList<>(); /** * Stores all parsed magnets. */ private ArrayList<Magnet> magnets = new ArrayList<>(); /** * Stores all parsed pin assignments. */ private HashMap<Integer, Pin> pins = new HashMap<>(); /** * Stores all parsed pinActuations. */ private HashMap<Integer, ActuationVector> pinActuations = new HashMap<>(); /** * Stores all parsed cellActuations. */ private HashMap<Point, ActuationVector> cellActuations = new HashMap<>(); /** * Stores all parsed areaAnnotations. */ private ArrayList<AreaAnnotation> areaAnnotations = new ArrayList<>(); /** * Stores all parsed mixers. */ private ArrayList<Mixer> mixers = new ArrayList<>(); /** * The biochip that will be filled with the stuff that was parsed. */ private Biochip chip; /** * Stores the textual representation of all errors that were found during * the parsing process. */ private List<String> errors; /** * Empty constructor. */ BioParserListener() { } /** * Gets all the errors. * * @return List of String containing all error messages. */ public List<String> getErrors() { return errors; } /** * Gets the parsed biochip. * * @return parsed biochip. */ public Biochip getBiochip() { return chip; } /** * Parses the given TimeConstraintContext. * * @param ctx * The TimeConstraintContext * @return int value of the timeConstraint */ private int getTimeConstraint(final TimeConstraintContext ctx) { return Integer.parseInt(ctx.Integer().getText()); } /** * Parses a given PositionContext. * * @param ctx * The positionContext * @return Point object for the position */ private Point getPosition(final PositionContext ctx) { Integer x = Integer.parseInt(ctx.xpos().getText()); Integer y = Integer.parseInt(ctx.ypos().getText()); return new Point(x, y); } /** * This gets an ID for Droplets, Fluids, Mixers and Pins. * <p> * The parameters should be of Type FluidIDContext, DropletIDContext, * MixerIDContext or PinIDContext, otherwise an IllegalArgumentException is * thrown. * * @param ctx * the context * @return the ID as int * @throws IllegalArgumentException * if the type is wrong */ private int getID(final ParserRuleContext ctx) { if (ctx == null) { return 0; } else { if (!(ctx instanceof FluidIDContext || ctx instanceof DropletIDContext || ctx instanceof PinIDContext || ctx instanceof MixerIDContext)) { logger.error("Could not parse an ID for the given type."); throw new IllegalArgumentException("Could not parse an ID for the given type."); } return Integer.parseInt(ctx.getToken(Bio.Integer, 0).getText()); } } /** * Parses the given SourceContext. * * @param ctx The SourceContext * @return Source object */ private Source getSource(final SourceContext ctx) { Point pos = getPosition(ctx.position()); int id = getID(ctx.dropletID()); if (ctx.timeConstraint() != null) { return new Source(id, pos, getTimeConstraint(ctx.timeConstraint()), new Point(1, 1) // in this case it is a "normal" droplet ); } else { return new Source(id, pos); } } /** * Parses the given AreaAnnotationContext. * * @param ctx The AreaAnnotationContext * @return AreaAnnotation object */ private AreaAnnotation getAreaAnnotation(final Bio.AreaAnnotationContext ctx) { Point pos1 = getPosition(ctx.position(0)); Point pos2 = pos1; if (ctx.position().size() > 1) { pos2 = getPosition(ctx.position(1)); } Rectangle rect = new Rectangle(pos1, pos2); return new AreaAnnotation(rect, ctx.AreaAnnotationText().getText()); } @Override public void enterDispenser(@NotNull final DispenserContext ctx) { int fluidID = getID(ctx.fluidID()); Pair<Point, Direction> dispenser = getIOPort(ctx.ioport()); if (dispenser != null) { updateMaxDimension(dispenser.fst); dispensers.add(new SimpleExternalResource(dispenser, fluidID)); } else { logger.error("Skipping definition of dispenser"); } } /** * Parses a String as a Direction. * * @param dir String resembling a direction * @return Direction object or null on error */ @Nullable private Direction getDirection(final String dir) { if ("N".equals(dir) || "U".equals(dir)) { return Direction.NORTH; } if ("E".equals(dir) || "R".equals(dir)) { return Direction.EAST; } if ("S".equals(dir) || "D".equals(dir)) { return Direction.SOUTH; } if ("W".equals(dir) || "L".equals(dir)) { return Direction.WEST; } logger.error("Could not parse \"{}\" as direction."); return null; } /** * Parses a given IoportContext. * * @param ctx The IoportContext * @return Pair with Point and Direction */ private Pair<Point, Direction> getIOPort(final Bio.IoportContext ctx) { Point pos = getPosition(ctx.position()); Direction dir = getDirection(ctx.Direction().getText()); if (dir == null) { return null; } return new Pair<>(pos, dir); } /** * Updates the MaxDimension values. It compares the given Point with the * stored maxX and maxY values. * * @param p A point with the coordinates to test. */ private void updateMaxDimension(final Point p) { maxX = Math.max(p.fst + 1, maxX); maxY = Math.max(p.snd + 1, maxY); } /** * Updates the max dimensions with two points. * * @param p1 The first point * @param p2 The second point */ private void updateMaxDimension(final Point p1, final Point p2) { updateMaxDimension(p1); updateMaxDimension(p2); } /** * Updates the count of parsed grids. * * @param ctx A GridContext */ @Override public void enterGrid(final Bio.GridContext ctx) { ++nGrids; } /** * Parses a given SinkContext. * * @param ctx The SinkContext */ @Override public void enterSink(@NotNull final Bio.SinkContext ctx) { Pair<Point, Direction> sinkDef = getIOPort(ctx.ioport()); if (sinkDef != null) { updateMaxDimension(sinkDef.fst); sinks.add(new SimpleExternalResource(sinkDef)); } else { logger.error("Skipping definition of sink"); } } /** * Parses a given AssignmentContext. * * @param ctx The AssignmentContext */ @Override public void enterAssignment(@NotNull final Bio.AssignmentContext ctx) { Point pos = getPosition(ctx.position()); int pinID = getID(ctx.pinID()); if (pins.containsKey(pinID)) { pins.get(pinID).cells.add(pos); } else { pins.put(pinID, new Pin(pinID, pos)); } } /** * Extracts a Rectangle from a list of PositionContext. * <p> * If the list contains exactly one element, it is treated as the single * point defining a 1 by 1 rectangle. If it contains two elements, they are * considered to be the defining opposite corners of a rectangle. Any * further elements are ignored. * <p> * The parameter positions is assumed to be non-null and having at least * one * element. If that criterion is not matched, this method will happily * crash. * * @param positions * List of PositionContext * @return The rectangle as specified in the list positions. */ private Rectangle extractRectangle(@NotNull final List<Bio.PositionContext> positions) { Point p = getPosition(positions.get(0)); Rectangle position; if (positions.size() > 1) { position = new Rectangle(p, getPosition(positions.get(1))); } else { position = new Rectangle(p, 1, 1); } return position; } /** * Parses a given MagnetContext. * <p> * The extracted magnet will be stored in the global heaters list. * * @param ctx * The MagnetContext. */ @Override public void enterMagnet(@NotNull final Bio.MagnetContext ctx) { List<Bio.PositionContext> positions = ctx.position(); Rectangle position = extractRectangle(positions); magnets.add(new Magnet(position)); } /** * Parses a given HeaterContext. * <p> * The extracted heater will be stored in the global heaters list. * * @param ctx * The HeaterContext. */ @Override public void enterHeater(@NotNull final Bio.HeaterContext ctx) { List<Bio.PositionContext> positions = ctx.position(); Rectangle position = extractRectangle(positions); heaters.add(new Heater(position)); } /** * Parses a given DetectorContext. * * @param ctx The DetectorContext */ @Override public void enterDetector(@NotNull final Bio.DetectorContext ctx) { List<PositionContext> positions = ctx.position(); Point pos = getPosition(positions.get(0)); Rectangle position; if (positions.size() > 1) { position = new Rectangle(pos, getPosition(positions.get(1))); } else { position = new Rectangle(pos, 1, 1); } int duration = 0; int fluidType = 0; Bio.Detector_specContext spec = ctx.detector_spec(); if (spec != null) { duration = getTimeConstraint(spec.timeConstraint()); if (spec.fluidID() == null) { fluidType = getID(spec.fluidID()); } } detectors.add(new Detector(position, duration, fluidType)); } /** * Parses a given DropToFluidContext. * * @param ctx The DropToFluidContext */ @Override public void enterDropToFluid(@NotNull final Bio.DropToFluidContext ctx) { int dropID = getID(ctx.dropletID()); int fluidID = getID(ctx.fluidID()); logger.debug("Adding droplet ID to fluid ID mapping: {} -> {}", dropID, fluidID); dropletIDsToFluidTypes.put(dropID, fluidID); } /** * Parses a NetContext. * * @param ctx The NetContext */ @Override public void enterNet(@NotNull final Bio.NetContext ctx) { Point target = getPosition(ctx.target().position()); ArrayList<Source> sources = new ArrayList<>(); ctx.children.stream().filter(child -> child instanceof Bio.SourceContext).forEach(child -> { Source src = getSource((Bio.SourceContext) child); sources.add(src); }); nets.add(new Net(sources, target)); } /** * Parses a LocationContext * @param loc the location context * @return a parsed rectangle */ Rectangle getLocation(@NotNull final Bio.LocationContext loc) { Point lowerLeft = getPosition(loc.position(0)); Point upperRight = getPosition(loc.position(1)); return new Rectangle(lowerLeft, upperRight); } @Override public void enterMedaNet(@NotNull final Bio.MedaNetContext ctx) { ArrayList<Source> sources = new ArrayList<>(); ctx.medaSource().forEach(c -> { int id = Integer.parseInt(c.dropletID().getText()); Rectangle srcRect = getLocation(c.location()); int spawnTime = 1; if (c.timeConstraint() != null) { spawnTime = getTimeConstraint(c.timeConstraint()); } Source src = new Source(id, srcRect, spawnTime); sources.add(src); }); Rectangle target = getLocation(ctx.medaTarget().location()); Net n = new Net(sources, target); logger.info("Adding new net: {}", n); nets.add(n); } /** * Parses a given BlockageContext. * * @param ctx The BlockageContext */ @Override public void enterBlockage(@NotNull final BlockageContext ctx) { Point p1 = getPosition((PositionContext) ctx.getChild(0)); Point p2 = getPosition((PositionContext) ctx.getChild(1)); Rectangle rect = new Rectangle(p1, p2); Range timing = getTiming((TimingContext) ctx.getChild(2)); logger.trace("Found blockage {} with timing {}", rect, timing); blockages.add(new Pair<>(rect, timing)); } /** * Parses a given TimingContext. * * @param ctx The TimingContext * @return Range object from begin to end or * DONTCARE to DONTCARE if ctx is null */ private Range getTiming(final TimingContext ctx) { if (ctx == null) { return new Range(Range.DONTCARE, Range.DONTCARE); } else { int begin = Range.DONTCARE; int end = Range.DONTCARE; TerminalNode beginTerm = ctx.beginTiming().Integer(); TerminalNode endTerm = ctx.endTiming().Integer(); if (beginTerm != null) { begin = Integer.parseInt(beginTerm.getText()); } if (endTerm != null) { end = Integer.parseInt(endTerm.getText()); } return new Range(begin, end); } } /** * Parses a given GridblockContext. * * @param ctx The GridblockContext */ @Override public void enterGridblock(final GridblockContext ctx) { Point p1 = getPosition((PositionContext) ctx.getChild(0)); Point p2 = getPosition((PositionContext) ctx.getChild(1)); updateMaxDimension(p1, p2); rectangles.add(new Rectangle(p1, p2)); super.enterGridblock(ctx); } /** * Parses a given FluiddefContext. * * @param ctx The FluiddefContext */ @Override public void enterFluiddef(@NotNull final FluiddefContext ctx) { int fluidID = Integer.parseInt(ctx.fluidID().getText()); String fluid = ctx.Identifier().getText(); logger.debug("Adding fluid identifier: {} -> {}", fluidID, fluid); fluidTypes.put(fluidID, fluid); } /** * Parses a given PinActuationContext. * * @param ctx The PinActuationContext. */ @Override public void enterPinActuation(@NotNull final PinActuationContext ctx) { int pinID = getID(ctx.pinID()); ActuationVector actVec = new ActuationVector(ctx.ActuationVector().getText()); pinActuations.put(pinID, actVec); } /** * Parses a given CellActuationContext. * * @param ctx The CellActuationContext */ @Override public void enterCellActuation(@NotNull final CellActuationContext ctx) { Point pos = getPosition(ctx.position()); ActuationVector actVec = new ActuationVector(ctx.ActuationVector().getText()); cellActuations.put(pos, actVec); } /** * Parses a given RouteContext. * * @param ctx The RouteContext */ @Override public void enterRoute(final RouteContext ctx) { int dropletID = Integer.parseInt(ctx.dropletID().getText()); int spawnTime = 1; if (ctx.timeConstraint() != null) { spawnTime = getTimeConstraint(ctx.timeConstraint()); } Droplet drop = new Droplet(dropletID, spawnTime); List<PositionContext> positions = ctx.position(); for (final PositionContext pos : positions) { Point p = getPosition(pos); Rectangle r = new Rectangle(p, 1, 1); drop.addPosition(r); } droplets.add(drop); } @Override public void enterMedaRoute(@NotNull final Bio.MedaRouteContext ctx) { int dropletID = Integer.parseInt(ctx.dropletID().getText()); int spawnTime = 1; if (ctx.timeConstraint() != null) { spawnTime = getTimeConstraint(ctx.timeConstraint()); } Droplet drop = new Droplet(dropletID, spawnTime); List<Bio.LocationContext> locations = ctx.location(); for (final Bio.LocationContext l : locations) { Point lowerLeft = getPosition(l.position(0)); Point upperRight = getPosition(l.position(1)); Rectangle r = new Rectangle(lowerLeft, upperRight); drop.addPosition(r); } droplets.add(drop); } /** * Parses a given TimeRangeContext. * * @param ctx The TimeRangeContext * @return Range from start to end */ private Range getTimeRange(final TimeRangeContext ctx) { Integer fst = Integer.parseInt(ctx.Integer(0).getText()); Integer snd = Integer.parseInt(ctx.Integer(1).getText()); logger.debug("Time range from {} to {}", ctx.Integer(0), ctx.Integer(1)); return new Range(fst, snd); } /** * Parses a given MixerContext. * * @param ctx The MixerContext */ @Override public void enterMixer(@NotNull final Bio.MixerContext ctx) { int id = getID(ctx.mixerID()); Rectangle rect = new Rectangle(getPosition(ctx.position(0)), getPosition(ctx.position(1))); Range time = getTimeRange(ctx.timeRange()); logger.debug("Received TimeRange {}", time); mixers.add(new Mixer(id, rect, time)); } /** * Parses a given AnnotationContext. * * @param ctx The AnnotationContext */ @Override public void enterAnnotations(@NotNull final Bio.AnnotationsContext ctx) { for (final Bio.AreaAnnotationContext areaCtx : ctx.areaAnnotation()) { areaAnnotations.add(getAreaAnnotation(areaCtx)); } } /** * Creates the BioChip when the parsing is done. * * @param ctx The BioContext */ @Override public void exitBio(final BioContext ctx) { chip = new Biochip(); errors = new ArrayList<>(); for (final Rectangle rect : rectangles) { for (final Point cell : rect.positions()) { chip.addField(new BiochipField(cell, chip)); } } if (nGrids > 1) { logger.warn("There were {} grid definitions in the file. The cells " + "were merged", nGrids); } droplets.forEach(chip::addDroplet); errors.addAll(Validator.checkPathsForJumps(droplets)); errors.addAll(Validator.checkPathsForPositions(droplets, chip.getAllCoordinates())); chip.addFluidTypes(fluidTypes); chip.addNets(nets); nets.forEach(net -> { Rectangle target = net.getTarget(); net.getSources().forEach(src -> { int dropID = src.dropletID; /* kind of weird code to set the net of a droplet. But this happens when the Java people think that they have a clever idea for 'streams' when normal people would simply directly operate on lists, maps etc. */ Optional<Droplet> drop = droplets.stream().filter(it -> it.getID() == dropID).findFirst(); drop.ifPresent(it -> it.setNet(net)); target.positions().forEach(p -> chip.getFieldAt(p).targetIDs.add(dropID)); src.startPosition.positions().forEach(p -> chip.getFieldAt(p).sourceIDs.add(dropID)); }); }); dropletIDsToFluidTypes.forEach(chip::addDropToFluid); errors.addAll(Validator.checkExternalResourcePositions(chip, sinks, true)); sinks.forEach(sink -> { Point sinkPoint = sink.resourcePosition(); Sink sinkField = new Sink(sinkPoint, chip); chip.addField(sinkField); }); errors.addAll(Validator.checkExternalResourcePositions(chip, dispensers, true)); dispensers.forEach(dispenser -> { Point dispPoint = dispenser.resourcePosition(); Dispenser dispField = new Dispenser(dispPoint, dispenser.id.get(), chip); chip.addField(dispField); }); for (final Pair<Rectangle, Range> b : blockages) { Rectangle rect = b.fst; Range rng = b.snd; rect.positions().forEach(pos -> chip.getFieldAt(pos).attachBlockage(rng)); } chip.blockages.addAll(blockages); errors.addAll(Validator.checkPathForBlockages(chip)); //##################################################################### // Resource adding begin /* In the following we will add detectors, heaters and magnets. If one of these resources is to be placed on a non-existing cell, an error will be logged and the offending resource is removed. If there are multiple resources for a single field they will happily override each other. (An error is logged at least). */ errors.addAll(Validator.checkForPositions(chip, "detector", detectors)); errors.addAll(Validator.checkForResources(chip, "detector", detectors)); detectors.forEach(det -> { List<Point> points = det.position.positions(); points.forEach(pos -> chip.getFieldAt(pos).setDetector(det)); }); chip.detectors.addAll(detectors); errors.addAll(Validator.checkForPositions(chip, "heater", heaters)); errors.addAll(Validator.checkForResources(chip, "heater", heaters)); chip.heaters.addAll(heaters); heaters.forEach(h -> { List<Point> points = h.position.positions(); points.forEach(pos -> chip.getFieldAt(pos).setHeater(h)); }); errors.addAll(Validator.checkForPositions(chip, "magnet", magnets)); errors.addAll(Validator.checkForResources(chip, "magnet", magnets)); chip.magnets.addAll(magnets); magnets.forEach(m -> { List<Point> points = m.position.positions(); points.forEach(pos -> chip.getFieldAt(pos).setMagnet(m)); }); // Resource adding end // #################################################################### pins.values().forEach(pin -> pin.cells.forEach(pos -> chip.getFieldAt(pos).pin = pin)); chip.pins.putAll(pins); errors.addAll(Validator.checkMultiplePinAssignments(pins.values())); chip.pinActuations.putAll(pinActuations); cellActuations.forEach((pos, vec) -> chip.getFieldAt(pos).actVec = vec); chip.cellActuations.putAll(cellActuations); errors.addAll(Validator.checkActuationVectorLengths(cellActuations, pinActuations)); errors.addAll(Validator.checkCellPinActuationCompatibility(chip, cellActuations, pinActuations, true)); errors.addAll(Validator.checkCellPinActuationCompatibility(chip, cellActuations, pinActuations, false)); errors.addAll(Validator.checkForPositions(chip, "Mixer", this.mixers)); chip.mixers.addAll(this.mixers); mixers.forEach(m -> m.position.positions().forEach(pos -> { logger.trace("Adding mixer {} to field {}", m, pos); chip.getFieldAt(pos).mixers.add(m); })); errors.addAll(Validator.checkForPositions(chip, "Annotation", this.areaAnnotations)); chip.areaAnnotations.addAll(this.areaAnnotations); areaAnnotations.forEach(a -> a.position.positions().forEach(pos -> { logger.trace("Adding areaAnnotation {} to " + "field {}", a, pos); chip.getFieldAt(pos).areaAnnotations.add(a); })); Set<FluidicConstraintViolation> badFields = chip.getAdjacentActivations(); for (final FluidicConstraintViolation violation : badFields) { errors.add(violation.toString()); } errors.forEach(logger::error); chip.errors.addAll(errors); } }