Java tutorial
/******************************************************************************* * Caleydo - Visualization for Molecular Biology - http://caleydo.org * Copyright (c) The Caleydo Team. All rights reserved. * Licensed under the new BSD license, available at http://caleydo.org/license ******************************************************************************/ package org.caleydo.view.bicluster.elem.band; import gleem.linalg.Vec2f; import gleem.linalg.Vec3f; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.caleydo.core.data.collection.EDimension; import org.caleydo.core.data.selection.SelectionManager; import org.caleydo.core.data.selection.SelectionType; import org.caleydo.core.event.EventListenerManager.ListenTo; import org.caleydo.core.event.EventPublisher; import org.caleydo.core.id.IIDTypeMapper; import org.caleydo.core.util.collection.Pair; import org.caleydo.core.util.color.Color; import org.caleydo.core.view.opengl.layout2.GLGraphics; import org.caleydo.core.view.opengl.layout2.IGLElementContext; import org.caleydo.core.view.opengl.layout2.PickableGLElement; import org.caleydo.core.view.opengl.layout2.util.PickingPool; import org.caleydo.core.view.opengl.picking.IPickingLabelProvider; import org.caleydo.core.view.opengl.picking.IPickingListener; import org.caleydo.core.view.opengl.picking.Pick; import org.caleydo.core.view.opengl.picking.PickingListenerComposite; import org.caleydo.core.view.opengl.util.gleem.ColoredVec3f; import org.caleydo.core.view.opengl.util.spline.Band; import org.caleydo.view.bicluster.elem.ClusterElement; import org.caleydo.view.bicluster.elem.Edge; import org.caleydo.view.bicluster.event.MouseOverBandEvent; import org.caleydo.view.bicluster.event.MouseOverClusterEvent; import org.caleydo.view.bicluster.internal.BiClusterRenderStyle; import org.caleydo.view.bicluster.util.SetUtils; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSortedSet; /** * @author Michael Gillhofer * */ public class BandElement extends PickableGLElement implements IPickingLabelProvider { private static final Color highlightColor = SelectionType.SELECTION.getColor(); private static final Color hoveredColor = SelectionType.MOUSE_OVER.getColor(); private static final float HIGH_OPACITY_FACTPOR = 1; private static final float LOW_OPACITY_FACTOR = 0.15f; private static final float DEFAULT_Z_DELTA = -4; private static final float HOVERED_BACKGROUND_Z_DELTA = -3; private static final float SELECTED_Z_DELTA = -2; private static final float HOVERED_Z_DELTA = -1; protected final Edge edge; private final EDimension dimension; protected List<Integer> overlap; private boolean hasSharedElementsWithSelection; private boolean hasSharedElementsWithHover; protected final SelectionManager selectionManager; private final IIDTypeMapper<Integer, String> id2label; protected Map<List<Integer>, Band> splittedBands; private Band band; protected Map<Integer, List<Vec2f>> splines; protected boolean isMouseOver = false; protected boolean isAnyThingHovered = false; protected PickingPool pickingPool; protected IPickingListener pickingListener; private int actSelectedSplineID = -1; private float targetOpacityFactor = 1; private BandFactory bandFactory; public BandElement(Edge edge, EDimension dimension, SelectionManager selectionManager, IIDTypeMapper<Integer, String> id2label) { this.edge = edge; this.dimension = dimension; this.id2label = id2label; this.selectionManager = selectionManager; hasSharedElementsWithSelection = false; hasSharedElementsWithHover = false; updateOverlap(); setZDeltaAccordingToState(); setPicker(null); } /** * @return the dimension, see {@link #dimension} */ public EDimension getDimension() { return dimension; } protected static ImmutableList<Integer> toFastOverlap(Collection<Integer> list) { return ImmutableSortedSet.copyOf(list).asList(); } /** * @return the first, see {@link #first} */ public final ClusterElement getFirst() { return edge.getA(); } /** * @return the second, see {@link #second} */ public final ClusterElement getSecond() { return edge.getB(); } @Override protected void init(IGLElementContext context) { pickingListener = new IPickingListener() { @Override public void pick(Pick pick) { onSplinePicked(pick); } }; pickingPool = new PickingPool(context, PickingListenerComposite.concat(pickingListener, context.getSWTLayer().createTooltip(this))); super.init(context); } @Override protected void takeDown() { pickingPool.clear(); super.takeDown(); } private void setZDeltaAccordingToState() { float oldZ = getzDelta(); float z = DEFAULT_Z_DELTA; if (hasSharedElementsWithHoveredBand()) z = HOVERED_BACKGROUND_Z_DELTA; if (hasSharedElementsWithSelectedBand()) z = SELECTED_Z_DELTA; if (isMouseOver) z = HOVERED_Z_DELTA; if (oldZ != z) { setzDelta(z); final AllBandsElement p = findAllBands(); if (p != null) p.triggerResort(); } } @Override public String getLabel(Pick pick) { int id = pick.getObjectID() - 1; Set<String> labels = id2label.apply(id); return StringUtils.join(labels, ","); } protected void onSplinePicked(Pick pick) { switch (pick.getPickingMode()) { case CLICKED: selectElement(SelectionType.SELECTION, pick.getObjectID(), true); repaint(); break; case MOUSE_OVER: this.actSelectedSplineID = pick.getObjectID(); repaint(); break; case MOUSE_OUT: this.actSelectedSplineID = -1; repaint(); break; default: break; } } @Override protected void onPicked(Pick pick) { switch (pick.getPickingMode()) { case CLICKED: onClicked(pick); break; case MOUSE_OUT: onMouseOut(pick); break; case MOUSE_OVER: onMouseOver(pick); break; default: return; } setZDeltaAccordingToState(); } @Override public void layout(int deltaTimeMs) { updateOpacticy(deltaTimeMs); super.layout(deltaTimeMs); } private void updateOpacticy(int deltaTimeMs) { float delta = Math.abs(actOpacityFactor - targetOpacityFactor); if (delta < 0.01f) // done return; final float speed = 0.003f; // [units/ms] float back = actOpacityFactor; final float change = deltaTimeMs * speed; if (targetOpacityFactor < actOpacityFactor) actOpacityFactor = Math.max(targetOpacityFactor, actOpacityFactor - change); else actOpacityFactor = Math.min(targetOpacityFactor, actOpacityFactor + change); if (back != actOpacityFactor) { repaint(); } } protected final void updateVisibilityByOverlap() { if (!overlap.isEmpty()) setVisibility(EVisibility.PICKABLE); else setVisibility(EVisibility.HIDDEN); } @Override protected void renderImpl(GLGraphics g, float w, float h) { Color bandColor; if (isVisible()) { if (hasSharedElementsWithSelectedBand()) bandColor = highlightColor; else if (hasSharedElementsWithHoveredBand()) bandColor = hoveredColor; else bandColor = BiClusterRenderStyle.getBandColor(dimension); // if (isMouseOver) { // g.color(bandColor.r, bandColor.g, bandColor.b, 0.8f * actOpacityFactor); // for (Band b : getSplittedBands().values()) { // g.drawPath(b); // } // g.color(bandColor.r, bandColor.g, bandColor.b, 0.5f * actOpacityFactor); // for (Band b : getSplittedBands().values()) { // g.fillPolygon(b); // } // // g.color(bandColor.r, bandColor.g, bandColor.b, 0.25f * actOpacityFactor); // List<Vec2f> currSelectedSpline = getSplines().get(actSelectedSplineID - 1); // for (List<Vec2f> b : getSplines().values()) { // if (b == currSelectedSpline) // continue; // g.drawPath(b, false); // } // if (currSelectedSpline != null) { // g.color(0, 0, 0, 0.85f * actOpacityFactor).lineWidth(2); // g.drawPath(currSelectedSpline, false); // g.lineWidth(1); // } // } else { // // } Color col = bandColor.clone(); col.a = 0.8f; g.color(bandColor.r, bandColor.g, bandColor.b, 0.8f * actOpacityFactor); Collection<Band> stubBands; if (!hasSelections()) // stub only if we haven't any highlights stubBands = stubify(band, col, actOpacityFactor, HIGH_OPACITY_FACTPOR); else { stubBands = ImmutableList.of(band); } for (Band b : stubBands) { g.drawPath(b); } g.color(bandColor.r, bandColor.g, bandColor.b, 0.5f * actOpacityFactor); for (Band b : stubBands) { g.fillPolygon(b); // } } } } private Map<Integer, List<Vec2f>> getSplines() { if (splines == null) splines = bandFactory.getConnectionsSplines(); return splines; } private Map<List<Integer>, Band> getSplittedBands() { if (splittedBands == null) splittedBands = bandFactory.getSplitableBands(); return splittedBands; } protected boolean isVisible() { return getFirst().isVisible() && getSecond().isVisible() && overlap != null && !overlap.isEmpty() && isValidBand(); } protected boolean hasSharedElementsWithSelectedBand() { return hasSharedElementsWithSelection; } protected boolean hasSharedElementsWithHoveredBand() { return hasSharedElementsWithHover; } @Override protected void renderPickImpl(GLGraphics g, float w, float h) { if (getVisibility() == EVisibility.PICKABLE && !isAnyThingHovered && isVisible()) { g.color(BiClusterRenderStyle.getBandColor(dimension)); // if (isMouseOver) { // for (Band b : getSplittedBands().values()) // g.fillPolygon(b); // g.incZ(); // for (Integer elementIndex : getSplines().keySet()) { // g.pushName(pickingPool.get(elementIndex + 1)); // g.fillPolygon(getSplines().get(elementIndex)); // g.popName(); // } // g.decZ(); // } else { { if (band != null) g.fillPolygon(band); } } } @Override protected void onClicked(Pick pick) { if (hasSharedElementsWithSelectedBand()) { // disable the selection again hasSharedElementsWithSelection = false; findAllBands().setSelection(null); selectElement(SelectionType.SELECTION, -1, true); } else { hasSharedElementsWithSelection = true; findAllBands().setSelection(this); selectElement(SelectionType.SELECTION, -1, true); } } protected AllBandsElement findAllBands() { return findParent(AllBandsElement.class); } protected void selectElement(SelectionType type, int objectId, boolean select) { if (selectionManager == null) return; findAllBands().clearAll(type); if (select) { if (objectId <= 0) selectionManager.addToType(type, overlap); else selectionManager.addToType(type, objectId - 1); } findAllBands().fireAllSelections(this); } public void deselect() { hasSharedElementsWithSelection = false; setZDeltaAccordingToState(); } @Override protected void onMouseOver(Pick pick) { isMouseOver = true; selectElement(SelectionType.MOUSE_OVER, -1, true); hasSharedElementsWithHover = true; EventPublisher.trigger(new MouseOverBandEvent(this, true)); repaintAll(); } @Override protected void onMouseOut(Pick pick) { isMouseOver = false; hasSharedElementsWithHover = false; this.actSelectedSplineID = -1; EventPublisher.trigger(new MouseOverBandEvent(this, false)); selectElement(SelectionType.MOUSE_OVER, -1, false); repaintAll(); } private boolean isValidBand() { return band != null && band.size() > 0 && !Float.isNaN(band.getCurveTop().get(0).x()); } public void updateStructure() { this.band = null; if (!updateOverlap()) return; ClusterElement first = edge.getA(); ClusterElement second = edge.getB(); if (!areValidBounds(first.getBounds()) || !areValidBounds(second.getBounds())) return; List<List<Integer>> firstSubIndices = first.getListOfContinousSequences(dimension, overlap); List<List<Integer>> secondSubIndices = second.getListOfContinousSequences(dimension, overlap); if (firstSubIndices.size() == 0) return; this.bandFactory = createFactory(dimension, first, second, firstSubIndices, secondSubIndices, overlap); this.band = bandFactory.getSimpleBand(); // lazy splittedBands = null; splines = null; if (pickingPool != null) { pickingPool.clear(); } // RECORD special // nonSplittedBands = bandFactory.getNonSplitableBands(); // // splittedBands = bandFactory.getSplitableBands(); // // // splitted bands are not looking really helpfull for records // splittedBands= nonSplittedBands; // // // splines = bandFactory.getConnectionsSplines(); // splines = new HashMap<>(); // create empty hashmap .. splines are not looking very good repaintAll(); } private boolean updateOverlap() { if (!edge.anyVisible()) return false; overlap = toFastOverlap(edge.getOverlapIndices(dimension)); updateVisibilityByOverlap(); return true; } private static BandFactory createFactory(EDimension dim, ClusterElement cluster, ClusterElement other, List<List<Integer>> firstSubIndices, List<List<Integer>> secondSubIndices, List<Integer> overlap) { return dim == EDimension.DIMENSION ? new DimensionBandFactory(cluster, other, firstSubIndices, secondSubIndices, overlap) : new RecordBandFactory(cluster, other, firstSubIndices, secondSubIndices, overlap); } public void updatePosition() { updateStructure(); } public void onSelectionUpdate(SelectionManager manager) { if (manager != selectionManager) return; hasSharedElementsWithHover = SetUtils.containsAny(overlap, selectionManager.getElements(SelectionType.MOUSE_OVER)); hasSharedElementsWithSelection = SetUtils.containsAny(overlap, selectionManager.getElements(SelectionType.SELECTION)); setZDeltaAccordingToState(); } protected float actOpacityFactor = 1; @ListenTo private void listenTo(MouseOverClusterEvent event) { isAnyThingHovered = event.isMouseOver(); if (!event.isMouseOver() || hasSelections()) { targetOpacityFactor = HIGH_OPACITY_FACTPOR; } else if (isNearEnough((ClusterElement) event.getSender())) targetOpacityFactor = HIGH_OPACITY_FACTPOR; else targetOpacityFactor = LOW_OPACITY_FACTOR; setZDeltaAccordingToState(); } /** * @param sender * @return */ private boolean isNearEnough(ClusterElement c) { if (c == getFirst() || c == getSecond()) return true; return c.nearEnough(getFirst(), getSecond()); // as symmetric } @ListenTo private void listenTo(MouseOverBandEvent event) { if (event.getSender() == this) return; isAnyThingHovered = event.isMouseOver(); boolean fadeOut = event.isMouseOver() && !hasSelections() && !isNearEnough(event.getFirst(), event.getSecond()); if (fadeOut) { targetOpacityFactor = LOW_OPACITY_FACTOR; // if (isOtherType(event.getBand())) // setVisibility(EVisibility.NONE); } else { targetOpacityFactor = HIGH_OPACITY_FACTPOR; // updateVisibilityByOverlap(); } setZDeltaAccordingToState(); } /** * @param first2 * @param second2 * @return */ private boolean isNearEnough(ClusterElement first, ClusterElement second) { ClusterElement f = getFirst(); ClusterElement s = getSecond(); if (first == f || first == s || second == f || second == s) return true; return first.nearEnough(f, s) || second.nearEnough(f, s); } // /** // * whether the bands are from different types // * // * @param band // * @return // */ // private boolean isOtherType(BandElement band) { // return !band.getClass().equals(this.getClass()); // } private boolean hasSelections() { return hasSharedElementsWithHoveredBand() || hasSharedElementsWithSelectedBand(); } protected Pair<Vec3f, Vec3f> pair(float x1, float y1, float x2, float y2) { Vec3f _1 = new Vec3f(x1, y1, 0); Vec3f _2 = new Vec3f(x2, y2, 0); return Pair.make(_1, _2); } /** * @param values * @param bandColor * @param curOpacityFactor2 * @return */ private static Collection<Band> stubify(Band band, Color color, float centerAlpha, float maxAlpha) { if (band == null || centerAlpha >= 1) return ImmutableList.of(band); Collection<Band> result = new ArrayList<>(2); stubify(result, band, color, centerAlpha, maxAlpha); return result; } private static void stubify(Collection<Band> result, Band band, Color color, float centerAlpha, float maxAlpha) { float r = color.r; float g = color.g; float b = color.b; float a = color.a; List<Vec3f> curveTop = band.getCurveTop(); List<Vec3f> curveBottom = band.getCurveBottom(); assert curveTop.size() == curveBottom.size(); final int size = curveTop.size(); List<Vec3f> cOut = new ArrayList<>(size); List<Vec3f> bOut = new ArrayList<>(size); boolean even = size % 2 == 0; int center = size % 2 == 0 ? (size / 2 - 1) : size / 2; float delta_a = (centerAlpha - maxAlpha) / (center * 0.3f); // artificial enlarge delta for better fading effect float act_a = 1; int firstAlpha = center; for (int i = 0; i < size; ++i) { Vec3f top = curveTop.get(i); Vec3f bottom = curveBottom.get(i); float a_i = Math.max(a * act_a, 0); if (a_i <= 0) firstAlpha = Math.min(firstAlpha, i); Color act = new Color(r, g, b, a_i); // System.out.println(i + " " + act_a); // manipulate act if (even && i == center) { // nothing } else if (i <= center) act_a += delta_a; else act_a -= delta_a; cOut.add(new ColoredVec3f(top, act)); bOut.add(new ColoredVec3f(bottom, act)); } // split the band into two to avoid tesselation effects result.add(new Band(cOut.subList(0, firstAlpha + 2), bOut.subList(0, firstAlpha + 2))); result.add(new Band(cOut.subList(size - firstAlpha - 1, size), bOut.subList(size - firstAlpha - 1, size))); } }