package nl.tudelft.lifetiles.graph.controller;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import javafx.fxml.FXML;
import javafx.scene.Group;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.scene.shape.Rectangle;
import nl.tudelft.lifetiles.annotation.model.GeneAnnotation;
import nl.tudelft.lifetiles.annotation.model.GeneAnnotationMapper;
import nl.tudelft.lifetiles.annotation.model.GeneAnnotationParser;
import nl.tudelft.lifetiles.annotation.model.KnownMutation;
import nl.tudelft.lifetiles.annotation.model.KnownMutationMapper;
import nl.tudelft.lifetiles.annotation.model.KnownMutationParser;
import nl.tudelft.lifetiles.core.controller.AbstractController;
import nl.tudelft.lifetiles.core.controller.MenuController;
import nl.tudelft.lifetiles.core.util.Message;
import nl.tudelft.lifetiles.core.util.Timer;
import nl.tudelft.lifetiles.graph.model.DefaultGraphParser;
import nl.tudelft.lifetiles.graph.model.FactoryProducer;
import nl.tudelft.lifetiles.graph.model.Graph;
import nl.tudelft.lifetiles.graph.model.GraphContainer;
import nl.tudelft.lifetiles.graph.model.GraphFactory;
import nl.tudelft.lifetiles.graph.model.GraphParser;
import nl.tudelft.lifetiles.graph.model.StackedMutationContainer;
import nl.tudelft.lifetiles.graph.view.DiagramView;
import nl.tudelft.lifetiles.graph.view.TileView;
import nl.tudelft.lifetiles.graph.view.VertexView;
import nl.tudelft.lifetiles.notification.controller.NotificationController;
import nl.tudelft.lifetiles.notification.model.NotificationFactory;
import nl.tudelft.lifetiles.sequence.controller.SequenceController;
import nl.tudelft.lifetiles.sequence.model.SegmentStringCollapsed;
import nl.tudelft.lifetiles.sequence.model.Sequence;
import nl.tudelft.lifetiles.sequence.model.SequenceSegment;
/**
* The controller of the graph view.
*
* @author Joren Hammudoglu
* @author AC Langerak
* @author Jos Winter
* @author Albert Smit
*
*/
public class GraphController extends AbstractController {
/**
* The message to display when operations are attempted without a graph
* being loaded.
*/
private static final String NOT_LOADED_MSG = "Graph not loaded"
+ " while attempting to add known mutations.";
/**
* The pane that will be used to draw the scrollpane and toolbar on the
* screen.
*/
@FXML
private BorderPane wrapper;
/**
* The scrollPane element.
*/
@FXML
private ScrollPane scrollPane;
/**
* The model of the graph.
*/
private GraphContainer model;
/**
* The view of the graph.
*/
private TileView view;
/**
* The model of the diagram.
*/
private StackedMutationContainer diagram;
/**
* The view of the diagram.
*/
private DiagramView diagramView;
/**
* graph model.
*/
private Graph<SequenceSegment> graph;
/**
* the highest unified coordinate in the graph.
*/
private long maxUnifiedEnd;
/**
* Current end position of a bucket.
*/
private int currEndPosition = -1;
/**
* Current start position of a bucket.
*/
private int currStartPosition = -1;
/**
* boolean to indicate if the controller must repaint the current position.
*/
private boolean repaintNow;
/**
* The initial value for zoomlevel.
*/
private static final int DEFAULTZOOMLEVEL = 45;
/**
* The current zoom level.
*/
private int zoomLevel = DEFAULTZOOMLEVEL;
/**
* The current zoom level, used to only redraw if the zoomlevel changes.
*/
private int currentZoomLevel;
/**
* Offset for initial scale.
*/
private static final double SCALE_OFFSET = 5;
/**
* The current scale to resize the graph.
*/
private double scale = Math.pow(ZOOM_OUT_FACTOR, zoomLevel - SCALE_OFFSET);
/**
* The currently inserted known mutations.
*/
private Map<SequenceSegment, List<KnownMutation>> knownMutations;
/**
* The currently inserted annotations.
*/
private Map<SequenceSegment, List<GeneAnnotation>> mappedAnnotations;
/**
* The factor that each zoom in step that updates the current scale.
*/
private static final double ZOOM_IN_FACTOR = 1.3125;
/**
* Visible sequences in the graph.
*/
private Set<Sequence> visibleSequences;
/**
* The current reference in the graph, shouted by the sequence control.
*/
private Sequence reference;
/**
* The mini map controller.
*/
private MiniMapController miniMapController;
/**
* Notification factory used to produce notifications in the graph
* controller.
*/
private NotificationFactory notifyFactory;
/**
* The factor that each zoom out step that updates the current scale.
*/
private static final double ZOOM_OUT_FACTOR = 1 / ZOOM_IN_FACTOR;
/**
* Maximal zoomed in level.
*/
private static final int MAX_ZOOM = 50;
private Zoombar toolbar;
/**
* {@inheritDoc}
*/
@Override
public void initialize(final URL location, final ResourceBundle resources) {
initListeners();
initZoomToolBar();
repaintNow = false;
scrollPane = new ScrollPane();
scrollPane.setOnScroll(event -> {
event.consume();
if (event.getDeltaY() > 0) {
toolbar.incrementZoom();
} else {
toolbar.decrementZoom();
}
});
}
/**
* Initialize the zoom toolbar.
*/
private void initZoomToolBar() {
toolbar = new Zoombar(zoomLevel, MAX_ZOOM);
wrapper.setRight(toolbar.getToolBar());
toolbar.getZoomlevel().addListener((observeVal, oldVal, newVal) -> {
int diffLevel = oldVal.intValue() - newVal.intValue();
zoomLevel = Math.abs(newVal.intValue());
if (diffLevel < 0) {
zoomGraph(Math.pow(ZOOM_OUT_FACTOR, diffLevel * -1));
} else if (diffLevel > 0) {
zoomGraph(Math.pow(ZOOM_IN_FACTOR, diffLevel));
}
});
}
/**
* @return the mini map controller
*/
private MiniMapController getMiniMapController() {
if (miniMapController == null) {
miniMapController = new MiniMapController(scrollPane, model);
}
return miniMapController;
}
/**
* Initialize the listeners.
*/
@SuppressWarnings("checkstyle:genericwhitespace")
private void initListeners() {
notifyFactory = new NotificationFactory();
listen(Message.OPENED, (controller, subject, args) -> {
assert controller instanceof MenuController;
switch (subject) {
case "graph":
openGraph(args);
maxUnifiedEnd = getMaxUnifiedEnd(graph);
break;
case "known mutations":
openKnownMutations(args);
break;
case "annotations":
openAnnotations(args);
break;
default:
return;
}
maxUnifiedEnd = getMaxUnifiedEnd(graph);
});
listen(Message.LOADED, (sender, subject, args) -> {
if (!subject.equals("sequences")) {
return;
}
assert (args[0] instanceof Map<?, ?>);
Map<String, Sequence> sequences = (Map<String, Sequence>) args[0];
if (model != null) {
model.setVisible(new HashSet<>(sequences.values()));
} else {
model = new GraphContainer(graph, null);
}
});
listen(Message.FILTERED, (controller, subject, args) -> {
assert args.length == 1;
assert args[0] instanceof Set<?>;
// unfortunately java doesn't really let us typecheck generics :(
@SuppressWarnings("unchecked")
Set<Sequence> newSequences = (Set<Sequence>) args[0];
visibleSequences = newSequences;
model.setVisible(visibleSequences);
diagram = new StackedMutationContainer(model.getBucketCache(),
visibleSequences);
repaintNow = true;
repaint();
});
listen(SequenceController.REFERENCE_SET,
(controller, subject, args) -> {
assert args.length == 1;
assert args[0] instanceof Sequence;
reference = (Sequence) args[0];
model = new GraphContainer(graph, reference);
model.setVisible(visibleSequences);
diagram = new StackedMutationContainer(model
.getBucketCache(), visibleSequences);
repaintNow = true;
repaint();
});
listen(Message.GOTO, (controller, subject, args) -> {
assert args[0] instanceof Long;
Long position = (Long) args[0];
// calculate position on a 0 to 1 scale
double hValue = position.doubleValue() / (double) maxUnifiedEnd;
scrollPane.setHvalue(hValue);
});
}
/**
* Function called if a graph file is opened.
* Loads the graph into the graph controller.
*
* @param args
* The arguments passed by the opened listener.
*/
private void openGraph(final Object... args) {
assert args.length == 2;
assert args[0] instanceof File && args[1] instanceof File;
try {
loadGraph((File) args[0], (File) args[1]);
} catch (IOException exception) {
shout(NotificationController.NOTIFY, "", notifyFactory
.getNotification(exception));
}
}
/**
* Function called if a known mutations file is opened.
* Loads and inserts the known mutations into the graph controller.
*
* @param args
* The arguments passed by the opened listener.
*/
private void openKnownMutations(final Object... args) {
assert args[0] instanceof File;
if (graph == null) {
shout(NotificationController.NOTIFY, "", notifyFactory
.getNotification(new IllegalStateException(NOT_LOADED_MSG)));
} else {
try {
insertKnownMutations((File) args[0]);
} catch (IOException exception) {
shout(NotificationController.NOTIFY, "", notifyFactory
.getNotification(exception));
}
}
}
/**
* Function called if a annotations file is opened.
* Loads and inserts the annotations into the graph controller.
*
* @param args
* The arguments passed by the opened listener.
*/
private void openAnnotations(final Object... args) {
assert args[0] instanceof File;
if (graph == null) {
shout(NotificationController.NOTIFY,
"",
notifyFactory
.getNotification(new IllegalStateException(
"Graph not loaded while attempting to add annotations.")));
} else {
try {
insertAnnotations((File) args[0]);
} catch (IOException exception) {
shout(NotificationController.NOTIFY, "", notifyFactory
.getNotification(exception));
}
}
}
/**
* @return the currently loaded graph.
*/
public Graph<SequenceSegment> getGraph() {
if (graph == null) {
throw new IllegalStateException("Graph not loaded.");
}
return graph;
}
/**
* Load a new graph from the specified file.
*
*
* @param vertexfile
* The file to get vertices for.
* @param edgefile
* The file to get edges for.
* @throws IOException
* When an IO error occurs while reading one of the files.
*/
private void loadGraph(final File vertexfile, final File edgefile)
throws IOException {
// create the graph
GraphFactory<SequenceSegment> factory = FactoryProducer.getFactory();
GraphParser parser = new DefaultGraphParser();
graph = parser.parseGraph(vertexfile, edgefile, factory);
collapseGraph(graph, parser.getSequences().size());
knownMutations = new HashMap<>();
mappedAnnotations = new HashMap<>();
model = new GraphContainer(graph, reference);
diagram = new StackedMutationContainer(model.getBucketCache(),
visibleSequences);
shout(Message.LOADED, "sequences", parser.getSequences());
repaintNow = true;
repaint();
}
/**
* Inserts a list of known mutations onto the graph from the specified file.
*
* @param file
* The file to get known mutations from.
* @throws IOException
* When an IO error occurs while reading one of the files.
*/
private void insertKnownMutations(final File file) throws IOException {
Timer timer = Timer.getAndStart();
List<KnownMutation> mutationsList = KnownMutationParser.parseKnownMutations(file);
knownMutations = KnownMutationMapper.mapAnnotations(graph,
mutationsList, reference);
timer.stopAndLog("Inserting known mutations");
shout(Message.LOADED, "known mutations", mutationsList);
repaintNow = true;
repaintPosition(scrollPane.hvalueProperty().doubleValue());
}
/**
* Collapses the total segments in the graph.
* Total segments contain all sequences in the graph.
*
* @param graph
* The graph to be collapsed.
* @param sequences
* The amount of sequences in the graph.
*/
private void collapseGraph(final Graph<SequenceSegment> graph,
final int sequences) {
for (SequenceSegment segment : graph.getAllVertices()) {
if (segment.getSources().size() == sequences) {
segment.setContent(new SegmentStringCollapsed(segment
.getContent()));
}
}
}
/**
* Inserts a list of annotations onto the graph from the specified file.
*
* @param file
* The file to get annotations from.
* @throws IOException
* When an IO error occurs while reading one of the files.
*/
private void insertAnnotations(final File file) throws IOException {
Timer timer = Timer.getAndStart();
List<GeneAnnotation> annotations = GeneAnnotationParser
.parseGeneAnnotations(file);
shout(Message.LOADED, "annotations", annotations);
mappedAnnotations = GeneAnnotationMapper.mapAnnotations(graph,
annotations, reference);
timer.stopAndLog("Inserting annotations");
repaintNow = true;
repaintPosition(scrollPane.hvalueProperty().doubleValue());
}
/**
* Repaints the view.
*/
private void repaint() {
wrapper.snapshot(new SnapshotParameters(), new WritableImage(5, 5));
if (graph != null) {
if (model == null) {
model = new GraphContainer(graph, reference);
}
if (diagram == null) {
diagram = new StackedMutationContainer(model.getBucketCache(),
visibleSequences);
}
view = new TileView(this,
wrapper.getBoundsInParent().getHeight() * 0.9);
diagramView = new DiagramView();
scrollPane.hvalueProperty().addListener(
(observable, oldValue, newValue) -> {
repaintPosition(newValue.doubleValue());
});
repaintPosition(scrollPane.hvalueProperty().doubleValue());
}
getMiniMapController().drawMiniMap();
}
/**
* Find the start and end bucket on the screenm given the position of the
* scrollbar.
*
* @param position
* horizontal position of the scrollbar
* @return an array where the first element is the start bucket and the last
* one is the end bucket
*/
private int[] getStartandEndBucket(final double position) {
double thumbSize = miniMapController.getMiniMap().getVisibleAmount();
double leftHalf = position - thumbSize;
double rightHalf = position + thumbSize;
if (leftHalf < 0) {
leftHalf /= 2;
}
if (rightHalf > scrollPane.getHmax()) {
rightHalf /= 2;
}
int[] buckets = new int[] {
getStartBucketPosition(leftHalf),
Math.min(model.getBucketCache().getNumberBuckets(),
getEndBucketPosition(rightHalf) + 2)
};
return buckets;
}
/**
* Repaints the view indicated by the bucket in the given position.
*
* @param position
* Position in the scrollPane.
*/
private void repaintPosition(final double position) {
int zoomSwitchLevel = MAX_ZOOM - diagram.getLevel();
double scaledVertex = scale * VertexView.HORIZONTALSCALE;
if (zoomLevel > zoomSwitchLevel) {
if (currentZoomLevel != zoomLevel || repaintNow) {
Group diagramDrawing = new Group();
double width = maxUnifiedEnd * scaledVertex;
int diagramLevel = zoomLevel - zoomSwitchLevel;
diagramDrawing.getChildren().addAll(
diagramView.drawDiagram(diagram, diagramLevel, width),
new Rectangle(width, 0));
scrollPane.setContent(diagramDrawing);
wrapper.setCenter(scrollPane);
currentZoomLevel = zoomLevel;
repaintNow = false;
}
} else {
int[] bucketLocations = getStartandEndBucket(position);
int startBucket = bucketLocations[0];
int endBucket = bucketLocations[1];
if (currEndPosition != endBucket
&& currStartPosition != startBucket || repaintNow) {
Group graphDrawing = new Group();
graphDrawing.setManaged(false);
graphDrawing.getChildren().addAll(
drawGraph(startBucket, endBucket),
new Rectangle(maxUnifiedEnd * scaledVertex, 0));
scrollPane.setContent(graphDrawing);
wrapper.setCenter(scrollPane);
currEndPosition = endBucket;
currStartPosition = startBucket;
repaintNow = false;
}
}
}
/**
* Zoom on the current graph given a zoomFactor.
*
* @param zoomFactor
* factor bigger than 1 makes the graph bigger
* between 0 and 1 makes the graph smaller
*/
private void zoomGraph(final double zoomFactor) {
scale *= zoomFactor;
repaintNow = true;
repaint();
}
/**
* Get the maximal unified end position based on the sinks of the graph.
*
* @param graph
* Graph for which the width must be calculated.
* @return the maximal unified end position.
*/
private long getMaxUnifiedEnd(final Graph<SequenceSegment> graph) {
long max = 0;
for (SequenceSegment vertex : graph.getSinks()) {
if (max < vertex.getUnifiedEnd()) {
max = vertex.getUnifiedEnd();
}
}
return max;
}
/**
* Return the start position in the bucket.
*
* @param position
* Position in the scrollPane.
* @return position in the bucket.
*/
private int getStartBucketPosition(final double position) {
return Math.max(0, model.getBucketCache()
.getBucketPosition(position));
}
/**
* Return the end position in the bucket.
*
* @param position
* Position in the scrollPane.
* @return position in the bucket.
*/
private int getEndBucketPosition(final double position) {
return Math.min(model.getBucketCache().getNumberBuckets(), model
.getBucketCache().getBucketPosition(position));
}
/**
* Creates a drawable object of the graph from the model.
*
* It will draw from the startBucket all the way to the endBucket.
*
* @param startBucket
* the first buket
* @param endBucket
* the last bucket
* @return Group object to be drawn on the screen
*/
public Group drawGraph(final int startBucket, final int endBucket) {
Group test = view.drawGraph(model.getVisibleSegments(startBucket,
endBucket), graph, knownMutations, mappedAnnotations, scale);
return test;
}
/**
* Set that this segment is selected and set those sequences visible.
*
* @param segment
* The selected segment
*/
public void clicked(final SequenceSegment segment) {
shout(Message.FILTERED, "", segment.getSources());
}
}