/** * Copyright (c) 2009, iPlant Collaborative, Texas Advanced Computing Center This software is licensed * under the CC-GNU GPL version 2.0 or later. License: http://creativecommons.org/licenses/GPL/2.0/ */ package org.iplantc.phyloviewer.client.tree.viewer; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.iplantc.core.broadcaster.shared.BroadcastCommand; import org.iplantc.core.broadcaster.shared.Broadcaster; import org.iplantc.phyloviewer.client.events.BranchClickEvent; import org.iplantc.phyloviewer.client.events.BranchClickHandler; import org.iplantc.phyloviewer.client.events.InteractionMode; import org.iplantc.phyloviewer.client.events.LabelClickEvent; import org.iplantc.phyloviewer.client.events.LabelClickHandler; import org.iplantc.phyloviewer.client.events.LeafClickEvent; import org.iplantc.phyloviewer.client.events.LeafClickHandler; import org.iplantc.phyloviewer.client.events.NodeClickEvent; import org.iplantc.phyloviewer.client.events.NodeClickHandler; import org.iplantc.phyloviewer.client.tree.viewer.canvas.Canvas; import org.iplantc.phyloviewer.client.tree.viewer.render.canvas.CanvasGraphics; import org.iplantc.phyloviewer.shared.layout.ILayoutData; import org.iplantc.phyloviewer.shared.math.Box2D; import org.iplantc.phyloviewer.shared.math.Matrix33; import org.iplantc.phyloviewer.shared.math.Vector2; import org.iplantc.phyloviewer.shared.model.IDocument; import org.iplantc.phyloviewer.shared.model.INode; import org.iplantc.phyloviewer.shared.render.Camera; import org.iplantc.phyloviewer.shared.render.CameraCladogram; import org.iplantc.phyloviewer.shared.render.Defaults; import org.iplantc.phyloviewer.shared.render.RenderPreferences; import org.iplantc.phyloviewer.shared.render.RenderTree; import org.iplantc.phyloviewer.shared.render.RenderTreeCladogram; import org.iplantc.phyloviewer.shared.render.style.GlyphStyle; import org.iplantc.phyloviewer.shared.render.style.IStyle; import org.iplantc.phyloviewer.shared.render.style.LabelStyle; import org.iplantc.phyloviewer.shared.render.style.Style; import org.iplantc.phyloviewer.shared.scene.Drawable; import org.iplantc.phyloviewer.shared.scene.DrawableContainer; import org.iplantc.phyloviewer.shared.scene.Text; import org.iplantc.phyloviewer.shared.scene.intersect.IntersectTree; import org.iplantc.phyloviewer.shared.scene.intersect.IntersectTreeBox; import org.iplantc.phyloviewer.shared.scene.intersect.IntersectTree.Hit; import com.google.gwt.core.client.Duration; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.DoubleClickHandler; import com.google.gwt.event.dom.client.HandlesAllKeyEvents; import com.google.gwt.event.dom.client.HandlesAllMouseEvents; import com.google.gwt.event.dom.client.MouseDownEvent; import com.google.gwt.event.dom.client.MouseDownHandler; import com.google.gwt.event.dom.client.MouseMoveEvent; import com.google.gwt.event.dom.client.MouseMoveHandler; import com.google.gwt.event.shared.EventBus; import com.google.gwt.event.shared.EventHandler; import com.google.gwt.event.shared.HandlerRegistration; public class DetailView extends AnimatedView implements Broadcaster { private int renderCount; private double[] renderTime = new double[60]; private boolean drawRenderStats = false; private Canvas canvas; private CanvasGraphics graphics = null; private RenderTree renderer = new RenderTreeCladogram(); private Map<EventHandler,List<HandlerRegistration>> handlerRegistrations = new HashMap<EventHandler,List<HandlerRegistration>>(); private InteractionMode currentInteractionMode; private BroadcastCommand broadcastCommand; private Hit lastHit; private int eventMask = 0; private Set<Drawable> overlays = new HashSet<Drawable>(); private CanvasGraphics overlayGraphics; private IStyle overlayStyle = new Style("overlay", null, new LabelStyle("#FF0000"), new GlyphStyle(Defaults.OVERVIEW_FILL_COLOR, Defaults.OVERVIEW_OUTLINE_COLOR, 1.0), null); public enum DrawableType { Point, Line, Polygon, Text } /** * Create a view with the given width and height. * * @param width * @param height */ public DetailView(int width, int height) { this.setStylePrimaryName("detailView"); this.setCamera(new CameraCladogram()); this.canvas = new Canvas(); graphics = new CanvasGraphics(canvas); overlayGraphics = new CanvasGraphics(canvas); this.resize(width, height); this.add(canvas); this.addMouseMoveHandler(new MouseMoveHandler() { @Override public void onMouseMove(MouseMoveEvent arg0) { IntersectTree intersector = createIntersector(arg0.getX(), arg0.getY()); intersector.intersect(); Hit hit = intersector.getClosestHit(); int x = arg0.getClientX(); int y = arg0.getClientY(); if(lastHit != null && hit != null) { // Check the drawables and make sure they are the same. if(lastHit.getDrawable() != hit.getDrawable()) { handleMouseOut(lastHit, x, y); handleMouseOver(hit, x, y); } } else if(lastHit == null && hit != null) { handleMouseOver(hit, x, y); } else if(lastHit != null && hit == null) { handleMouseOut(lastHit, x, y); } lastHit = hit; } }); this.addMouseDownHandler(new MouseDownHandler() { @Override public void onMouseDown(MouseDownEvent arg0) { if(arg0.getNativeButton() == 1) { Hit hit = lastHit; int x = arg0.getClientX(); int y = arg0.getClientY(); handleMouseClick(hit, x, y); } } }); } public void render() { try { if(this.isReady()) { Duration duration = new Duration(); Matrix33 viewMatrix = new Matrix33(); Camera camera = getCamera(); if(camera != null) { viewMatrix = camera.getMatrix(getWidth(), getHeight()); } renderer.renderTree(graphics, viewMatrix); overlayGraphics.clearDrawnTextExtents(); if(drawRenderStats) { renderStats(duration.elapsedMillis()); } for (Drawable drawable : overlays) { drawable.draw(overlayGraphics, overlayStyle); } } } catch(Exception e) { Logger.getLogger("").log(Level.WARNING, "An exception was caught in DetailView.render: " + e.getMessage()); } } public void resize(int width, int height) { graphics.setSize(width, height); overlayGraphics.setSize(width, height); setCanvasSize(width, height); } protected void setCanvasSize(int width, int height) { canvas.setWidth(width); canvas.setHeight(height); } protected int getHeight() { return canvas.getHeight(); } protected int getWidth() { return canvas.getWidth(); } protected RenderTree getRenderer() { return renderer; } public void setRenderer(RenderTree renderer) { this.renderer = renderer; } @Override public void setDocument(IDocument document) { super.setDocument(document); this.getCamera().reset(); if(renderer != null) { renderer.setDocument(document); } } @Override public boolean isReady() { boolean documentReady = this.getDocument() != null && this.getDocument().isReady(); boolean ready = documentReady && graphics != null && getCamera() != null; return ready; } private void renderStats(double time) { renderCount++; int index = renderCount % 60; renderTime[index] = time; String text = renderCount + " frames, last: " + Math.round(1.0 / time * 1000) + " FPS"; if(renderCount >= 60) { double totalTime = 0; for(double t : renderTime) { totalTime += t; } double fps = (60.0 / totalTime) * 1000; text += " average: " + Math.round(fps) + " FPS"; } Text textDrawable = new Text(text, new Vector2(0, overlayGraphics.getHeight()), new Vector2(5, -5)); textDrawable.draw(overlayGraphics, overlayStyle); } public String exportImageURL() { return canvas.toDataURL(); } public void setRenderPreferences(RenderPreferences rp) { super.setRenderPreferences(rp); renderer.setRenderPreferences(rp); } /** * @return the node at the given location, in this View's screen coordinate system */ public INode getNodeAt(int x, int y) { IntersectTree intersector = createIntersector(x, y); intersector.intersect(); Hit hit = intersector.getClosestHit(); return hit != null ? hit.getNode() : null; } /** * Create an intersector * * @param x position in screen coordinates * @param y position in screen coordinates * @return An object to perform intersections. */ private IntersectTree createIntersector(int x, int y) { Vector2 position = getPositionInLayoutSpace(new Vector2(x, y)); // Calculate the maximum size of a pixel side Vector2 v0 = getPositionInLayoutSpace(new Vector2(0, 0)); Vector2 v1 = getPositionInLayoutSpace(new Vector2(1, 1)); Vector2 distanceInObjectSpace = v1.subtract(v0); double distance = Math.max(distanceInObjectSpace.getX(), distanceInObjectSpace.getY()); DrawableContainer container = renderer != null ? renderer.getDrawableContainer() : null; IntersectTree intersector = new IntersectTree(getDocument(), container, position, distance); return intersector; } public Set<INode> getNodesIn(Box2D screenBox) { Set<INode> nodes = Collections.emptySet(); if(getTree() != null) { Box2D range = getBoxInLayoutSpace(screenBox); INode root = getTree().getRootNode(); ILayoutData layout = getLayout(); nodes = IntersectTreeBox.intersect(root, layout, range); } return nodes; } public Vector2 getPositionInLayoutSpace(Vector2 position) { if(graphics != null) { Matrix33 IM = graphics.getScreenToObjectMatrix(); return IM.transform(position); } return position; } public Box2D getBoxInLayoutSpace(Box2D box) { Vector2 min = getPositionInLayoutSpace(box.getMin()); Vector2 max = getPositionInLayoutSpace(box.getMax()); return new Box2D(min, max); } public void setInteractionMode(InteractionMode mode) { if (currentInteractionMode != null) { unregister(currentInteractionMode.getMouseHandler()); unregister(currentInteractionMode.getKeyHandler()); removeStyleName(currentInteractionMode.getStyleName()); } this.addMouseHandler(mode.getMouseHandler()); this.addKeyboardHandler(mode.getKeyHandler()); addStyleName(mode.getStyleName()); this.currentInteractionMode = mode; } private void addMouseHandler(HandlesAllMouseEvents handler) { List<HandlerRegistration> registrations = handlerRegistrations.get(handler); if(registrations == null) { registrations = new ArrayList<HandlerRegistration>(); handlerRegistrations.put(handler, registrations); } // add this handler for all supported events registrations.add(this.addMouseDownHandler(handler)); registrations.add(this.addMouseUpHandler(handler)); registrations.add(this.addMouseOutHandler(handler)); registrations.add(this.addMouseOverHandler(handler)); registrations.add(this.addMouseMoveHandler(handler)); registrations.add(this.addMouseWheelHandler(handler)); if(handler instanceof ClickHandler) { registrations.add(this.addClickHandler((ClickHandler)handler)); } if(handler instanceof DoubleClickHandler) { registrations.add(this.addDoubleClickHandler((DoubleClickHandler)handler)); } } private void addKeyboardHandler(HandlesAllKeyEvents handler) { List<HandlerRegistration> registrations = handlerRegistrations.get(handler); if(registrations == null) { registrations = new ArrayList<HandlerRegistration>(); handlerRegistrations.put(handler, registrations); } registrations.add(this.addKeyDownHandler(handler)); registrations.add(this.addKeyPressHandler(handler)); registrations.add(this.addKeyUpHandler(handler)); } /** * Removes all handler registrations for the given handler */ private void unregister(EventHandler handler) { List<HandlerRegistration> registrations = handlerRegistrations.get(handler); if(registrations != null) { for(HandlerRegistration registration : registrations) { registration.removeHandler(); } } registrations.clear(); } @Override protected void initEventListeners() { super.initEventListeners(); EventBus eventBus = getEventBus(); if(eventBus != null) { eventBus.addHandler(NodeClickEvent.TYPE, new NodeClickHandler() { @Override public void onNodeClick(NodeClickEvent event) { broadcastEvent("node_clicked", event.getNodeId(), event.getClientX(), event.getClientY(), event.getMetaDataString()); } }); eventBus.addHandler(LeafClickEvent.TYPE, new LeafClickHandler() { @Override public void onLeafClick(LeafClickEvent event) { broadcastEvent("leaf_clicked", event.getNodeId(), event.getClientX(), event.getClientY(), event.getMetaDataString()); } }); eventBus.addHandler(BranchClickEvent.TYPE, new BranchClickHandler() { @Override public void onBranchClick(BranchClickEvent event) { broadcastEvent("branch_clicked", event.getNodeId(), event.getClientX(), event.getClientY(), event.getMetaDataString()); } }); eventBus.addHandler(LabelClickEvent.TYPE, new LabelClickHandler() { @Override public void onLabelClick(LabelClickEvent event) { broadcastEvent("label_clicked", event.getNodeId(), event.getClientX(), event.getClientY(), event.getMetaDataString()); } }); } } private void broadcastEvent(String type, int id, int clientX, int clientY, String metaDataString) { if(broadcastCommand != null) { String metadata = metaDataString != null ? metaDataString: ""; String json = "{\"event\":\"" + type + "\",\"id\":\"" + id + "\",\"mouse\":{\"x\":" + clientX + ",\"y\":" + clientY + "}" + ",\"metadata\":" + metadata + "}"; broadcastCommand.broadcast(json); } } @Override public void setBroadcastCommand(BroadcastCommand cmdBroadcast) { this.broadcastCommand = cmdBroadcast; } /** * Clear all highlighted nodes. */ public void clearHighlights() { RenderTree renderer = this.getRenderer(); if(renderer != null) { RenderPreferences prefs = renderer.getRenderPreferences(); if(prefs != null) { prefs.clearAllHighlights(); this.requestRender(); } } } /** * Highlight given node * * @param node id */ public void highlightNode(Integer id) { RenderTree renderer = this.getRenderer(); if(renderer != null) { RenderPreferences prefs = renderer.getRenderPreferences(); if(prefs != null) { prefs.highlightNode(id); this.requestRender(); } } } /** * Highlight node and subtree for given id. * * @param node id */ public void highlightSubtree(Integer id) { RenderTree renderer = this.getRenderer(); if(renderer != null) { RenderPreferences prefs = renderer.getRenderPreferences(); if(prefs != null) { prefs.highlightSubtree(id); this.requestRender(); } } } /** * Highlight branch to given node id. * * @param node id */ public void highlightBranch(Integer id) { RenderTree renderer = this.getRenderer(); if(renderer != null) { RenderPreferences prefs = renderer.getRenderPreferences(); if(prefs != null) { prefs.highlightBranch(id); this.requestRender(); } } } public boolean isDrawRenderStats() { return drawRenderStats; } public void setDrawRenderStats(boolean drawRenderStats) { this.drawRenderStats = drawRenderStats; } private void handleMouseClick(Hit hit, int x, int y) { if(hit != null && hit.getDrawable() != null && isEventTypeAllowed(hit)) { INode node = hit.getNode(); Drawable.Context context = hit.getDrawable().getContext(); if(node != null) { String metaDataString = node.getMetaDataString(); int nodeId = node.getId(); if(Drawable.Context.CONTEXT_NODE == context) { if(node.isLeaf()) { dispatch(new LeafClickEvent(nodeId, x, y, metaDataString)); } else { dispatch(new NodeClickEvent(nodeId, x, y, metaDataString)); } } else if(Drawable.Context.CONTEXT_BRANCH == context) { dispatch(new BranchClickEvent(nodeId, x, y, metaDataString)); } else if(Drawable.Context.CONTEXT_LABEL == context) { dispatch(new LabelClickEvent(nodeId, x, y, metaDataString)); } } } } private void handleMouseOver(Hit hit, int x, int y) { if(hit != null && hit.getDrawable() != null && isEventTypeAllowed(hit)) { INode node = hit.getNode(); Drawable.Context context = hit.getDrawable().getContext(); if(node != null) { String metaDataString = node.getMetaDataString(); int nodeId = node.getId(); if(Drawable.Context.CONTEXT_NODE == context) { if(node.isLeaf()) { broadcastEvent("leaf_mouse_over", nodeId, x, y, metaDataString); } else { broadcastEvent("node_mouse_over", nodeId, x, y, metaDataString); } } else if(Drawable.Context.CONTEXT_BRANCH == context) { broadcastEvent("branch_mouse_over", nodeId, x, y, metaDataString); } else if(Drawable.Context.CONTEXT_LABEL == context) { broadcastEvent("label_mouse_over", nodeId, x, y, metaDataString); } } } } private void handleMouseOut(Hit hit, int x, int y) { if(hit != null && hit.getDrawable() != null && isEventTypeAllowed(hit)) { INode node = hit.getNode(); Drawable.Context context = hit.getDrawable().getContext(); if(node != null) { String metaDataString = node.getMetaDataString(); int nodeId = node.getId(); if(Drawable.Context.CONTEXT_NODE == context) { if(node.isLeaf()) { broadcastEvent("leaf_mouse_out", nodeId, x, y, metaDataString); } else { broadcastEvent("node_mouse_out", nodeId, x, y, metaDataString); } } else if(Drawable.Context.CONTEXT_BRANCH == context) { broadcastEvent("branch_mouse_out", nodeId, x, y, metaDataString); } else if(Drawable.Context.CONTEXT_LABEL == context) { broadcastEvent("label_mouse_out", nodeId, x, y, metaDataString); } } } } private boolean isEventTypeAllowed(Hit hit) { if(hit != null && hit.getDrawable() != null) { return (eventMask & hit.getDrawable().getDrawableType()) == 0; } return false; } /** * Filter out events that correspond to the drawable type * * @param type */ public void addEventFilter(DrawableType type) { switch (type) { case Point: eventMask = eventMask | Drawable.TYPE_POINT; break; case Line: eventMask = eventMask | Drawable.TYPE_LINE; break; case Polygon: eventMask = eventMask | Drawable.TYPE_POLYGON; break; case Text: eventMask = eventMask | Drawable.TYPE_TEXT; break; } } /** * Allow all events. */ public void clearEventFilters() { this.eventMask = 0; } public boolean addOverlay(Drawable d) { return overlays.add(d); } public boolean removeOverlay(Drawable d) { return overlays.remove(d); } }