Java tutorial
/******************************************************************************* * Copyright 2009, 2010 Lars Grammel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *******************************************************************************/ package org.thechiselgroup.choosel.visualization_component.graph.client; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.thechiselgroup.choosel.core.client.command.CommandManager; import org.thechiselgroup.choosel.core.client.geometry.DefaultSize; import org.thechiselgroup.choosel.core.client.geometry.Point; import org.thechiselgroup.choosel.core.client.geometry.Size; import org.thechiselgroup.choosel.core.client.persistence.Memento; import org.thechiselgroup.choosel.core.client.persistence.PersistableRestorationService; import org.thechiselgroup.choosel.core.client.resources.DefaultResourceSet; import org.thechiselgroup.choosel.core.client.resources.Resource; import org.thechiselgroup.choosel.core.client.resources.ResourceCategorizer; import org.thechiselgroup.choosel.core.client.resources.ResourceManager; import org.thechiselgroup.choosel.core.client.resources.ResourceSet; import org.thechiselgroup.choosel.core.client.resources.UnionResourceSet; import org.thechiselgroup.choosel.core.client.resources.persistence.ResourceSetAccessor; import org.thechiselgroup.choosel.core.client.resources.persistence.ResourceSetCollector; import org.thechiselgroup.choosel.core.client.ui.SidePanelSection; import org.thechiselgroup.choosel.core.client.util.DataType; import org.thechiselgroup.choosel.core.client.util.NoSuchAdapterException; import org.thechiselgroup.choosel.core.client.util.collections.CollectionFactory; import org.thechiselgroup.choosel.core.client.util.collections.Delta; import org.thechiselgroup.choosel.core.client.util.collections.LightweightCollection; import org.thechiselgroup.choosel.core.client.visualization.model.AbstractViewContentDisplay; import org.thechiselgroup.choosel.core.client.visualization.model.Slot; import org.thechiselgroup.choosel.core.client.visualization.model.ViewContentDisplayCallback; import org.thechiselgroup.choosel.core.client.visualization.model.VisualItem; import org.thechiselgroup.choosel.core.client.visualization.model.VisualItemContainer; import org.thechiselgroup.choosel.core.client.visualization.model.VisualItemInteraction; import org.thechiselgroup.choosel.core.client.visualization.model.VisualItemInteraction.Type; import org.thechiselgroup.choosel.core.client.visualization.model.extensions.RequiresAutomaticResourceSet; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.GraphDisplay; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.GraphDisplayLoadingFailureEvent; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.GraphDisplayLoadingFailureEventHandler; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.GraphDisplayReadyEvent; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.GraphDisplayReadyEventHandler; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.GraphLayouts; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.GraphWidget; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.Node; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.NodeDragEvent; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.NodeDragHandleMouseDownEvent; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.NodeDragHandleMouseDownHandler; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.NodeDragHandleMouseMoveEvent; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.NodeDragHandleMouseMoveHandler; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.NodeDragHandler; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.NodeEvent; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.NodeMenuItemClickedHandler; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.NodeMouseClickEvent; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.NodeMouseClickHandler; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.NodeMouseOutEvent; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.NodeMouseOutHandler; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.NodeMouseOverEvent; import org.thechiselgroup.choosel.visualization_component.graph.client.widget.NodeMouseOverHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.MouseMoveEvent; import com.google.gwt.event.dom.client.MouseMoveHandler; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; import com.google.inject.Inject; // TODO separate out ncbo specific stuff and service calls // TODO register listener for double click on node --> change expansion state public class Graph extends AbstractViewContentDisplay implements RequiresAutomaticResourceSet, GraphLayoutSupport, GraphLayoutCallback { public static class DefaultDisplay extends GraphWidget implements GraphDisplay { // TODO why is size needed in the first place?? public DefaultDisplay() { this(400, 300); } public DefaultDisplay(int width, int height) { super(width, height); } } private class GraphEventHandler implements NodeMouseOverHandler, NodeMouseOutHandler, NodeMouseClickHandler, MouseMoveHandler, NodeDragHandler, NodeDragHandleMouseDownHandler, NodeDragHandleMouseMoveHandler { /** * Node that the mouse is currently over. Set by mouse out and mouse * over. Not over a node if null. */ private Node currentNode = null; @Override public void onDrag(NodeDragEvent event) { commandManager.execute(new MoveNodeCommand(graphDisplay, event.getNode(), new Point(event.getStartX(), event.getStartY()), new Point(event.getEndX(), event.getEndY()))); } @Override public void onMouseClick(NodeMouseClickEvent event) { reportInteraction(Type.CLICK, event); } @Override public void onMouseDown(NodeDragHandleMouseDownEvent event) { reportInteraction(Type.MOUSE_DOWN, event); } @Override public void onMouseMove(MouseMoveEvent event) { if (currentNode != null) { getVisualItem(currentNode).reportInteraction( new VisualItemInteraction(Type.MOUSE_MOVE, event.getClientX(), event.getClientY())); } } @Override public void onMouseMove(NodeDragHandleMouseMoveEvent event) { reportInteraction(Type.MOUSE_MOVE, event); } @Override public void onMouseOut(NodeMouseOutEvent event) { currentNode = null; reportInteraction(Type.MOUSE_OUT, event); } @Override public void onMouseOver(NodeMouseOverEvent event) { currentNode = event.getNode(); reportInteraction(Type.MOUSE_OVER, event); } private void reportInteraction(Type eventType, NodeEvent<?> event) { int clientX = event.getMouseX() + asWidget().getAbsoluteLeft(); int clientY = event.getMouseY() + asWidget().getAbsoluteTop(); getVisualItem(event).reportInteraction(new VisualItemInteraction(eventType, clientX, clientY)); } } public class GraphLayoutAction implements ViewContentDisplayAction { private String layout; public GraphLayoutAction(String layout) { this.layout = layout; } @Override public void execute() { commandManager.execute(new GraphLayoutCommand(graphDisplay, layout, getAllNodes())); } @Override public String getLabel() { return layout; } } public static final Slot NODE_BORDER_COLOR = new Slot("nodeBorderColor", "Node Border Color", DataType.COLOR); public static final Slot NODE_BACKGROUND_COLOR = new Slot("nodeBackgroundColor", "Node Color", DataType.COLOR); public static final Slot NODE_LABEL_SLOT = new Slot("nodeLabel", "Node Label", DataType.TEXT); public final static String ID = "org.thechiselgroup.choosel.visualization_component.graph.Graph"; private static final String MEMENTO_ARC_ITEM_CONTAINERS_CHILD = "arcItemContainers"; private static final String MEMENTO_NODE_LOCATIONS_CHILD = "nodeLocations"; private static final String MEMENTO_X = "x"; private static final String MEMENTO_Y = "y"; // TODO move public static String getArcId(String arcType, String sourceId, String targetId) { // FIXME this needs escaping of special characters to work properly return arcType + ":" + sourceId + "_" + targetId; } private ArcTypeProvider arcStyleProvider; private final CommandManager commandManager; // advanced node class: (incoming, outgoing, expanded: state machine) private final GraphDisplay graphDisplay; private boolean ready = false; private GraphExpansionRegistry registry; private ResourceCategorizer resourceCategorizer; private ResourceManager resourceManager; private UnionResourceSet nodeResources = new UnionResourceSet(new DefaultResourceSet()); private Map<String, ArcItemContainer> arcItemContainersByArcTypeID = CollectionFactory.createStringMap(); private ResourceSet automaticResources; /* * TODO The callback is meant to check whether the graph is initialized (and * not disposed) when methods are called (to prevent errors in asynchronous * callbacks that return after the graph has been disposed or before it has * been initialized). */ private GraphNodeExpansionCallback expansionCallback = new GraphNodeExpansionCallback() { @Override public void addAutomaticResource(Resource resource) { Graph.this.addAutomaticResource(resource); } @Override public boolean containsResourceWithUri(String resourceUri) { return Graph.this.containsResourceWithUri(resourceUri); } @Override public String getCategory(Resource resource) { return Graph.this.getCategory(resource); } @Override public GraphDisplay getDisplay() { return Graph.this.getDisplay(); } @Override public Resource getResourceByUri(String value) { return Graph.this.getResourceByUri(value); } @Override public ResourceManager getResourceManager() { return Graph.this.getResourceManager(); } @Override public LightweightCollection<VisualItem> getVisualItems(Iterable<Resource> resources) { return Graph.this.getVisualItems(resources); } @Override public boolean isInitialized() { return Graph.this.isInitialized(); } @Override public boolean isRestoring() { return Graph.this.isRestoring(); } @Override public void updateArcsForResources(Iterable<Resource> resources) { Graph.this.updateArcsForResources(resources); } @Override public void updateArcsForVisuaItems(LightweightCollection<VisualItem> visualItems) { Graph.this.updateArcsForVisuaItems(visualItems); } }; @Inject public Graph(GraphDisplay display, CommandManager commandManager, ResourceManager resourceManager, ResourceCategorizer resourceCategorizer, ArcTypeProvider arcStyleProvider, GraphExpansionRegistry registry) { assert display != null; assert commandManager != null; assert resourceManager != null; assert resourceCategorizer != null; assert arcStyleProvider != null; assert registry != null; this.arcStyleProvider = arcStyleProvider; this.resourceCategorizer = resourceCategorizer; graphDisplay = display; this.commandManager = commandManager; this.resourceManager = resourceManager; this.registry = registry; /* * we init the arc type containers early so they are available for UI * customization in Choosel applications. */ initArcTypeContainers(); } @SuppressWarnings("unchecked") @Override public <T> T adaptTo(Class<T> clazz) throws NoSuchAdapterException { if (GraphLayoutSupport.class.equals(clazz)) { return (T) this; } return super.adaptTo(clazz); } private void addAutomaticResource(Resource resource) { automaticResources.add(resource); } private boolean containsResourceWithUri(String resourceUri) { return nodeResources.containsResourceWithUri(resourceUri); } private NodeItem createGraphNodeItem(VisualItem visualItem) { // TODO get from group id String type = getCategory(visualItem.getResources().getFirstElement()); NodeItem graphItem = new NodeItem(visualItem, type, graphDisplay); graphDisplay.addNode(graphItem.getNode()); positionNode(graphItem.getNode()); // TODO re-enable // TODO remove once new drag and drop mechanism works... graphDisplay.setNodeStyle(graphItem.getNode(), "showDragImage", "false"); graphDisplay.setNodeStyle(graphItem.getNode(), "showArrow", registry.getNodeMenuEntries(type).isEmpty() ? "false" : "true"); nodeResources.addResourceSet(visualItem.getResources()); visualItem.setDisplayObject(graphItem); /* * NOTE: all node configuration should be done when calling the * automatic expanders, since they rely on returning the correct graph * contents etc. * * NOTE: we do not execute the expanders if we are restoring the graph */ if (ready) { registry.getAutomaticExpander(type).expand(visualItem, expansionCallback); } return graphItem; } // TODO encapsulate in display, use dependency injection @Override protected Widget createWidget() { return graphDisplay.asWidget(); } // TODO better caching? // default visibility for test case use List<Node> getAllNodes() { List<Node> result = new ArrayList<Node>(); for (VisualItem visualItem : getVisualItems()) { result.add(getNode(visualItem)); } return result; } public ResourceSet getAllResources() { return nodeResources; } public ArcItemContainer getArcItemContainer(String arcTypeID) { assert arcTypeID != null; assert arcItemContainersByArcTypeID.containsKey(arcTypeID); return arcItemContainersByArcTypeID.get(arcTypeID); } public Iterable<ArcItemContainer> getArcItemContainers() { return arcItemContainersByArcTypeID.values(); } private ArcItem[] getArcItems() { Iterable<ArcItemContainer> arcItemContainers = getArcItemContainers(); int size = 0; for (ArcItemContainer arcItemContainer : arcItemContainers) { size += arcItemContainer.getArcItems().size(); } ArcItem[] arcs = new ArcItem[size]; int i = 0; for (ArcItemContainer arcItemContainer : arcItemContainers) { for (ArcItem arcItem : arcItemContainer.getArcItems()) { arcs[i++] = arcItem; } } return arcs; } private String getCategory(Resource resource) { return resourceCategorizer.getCategory(resource); } private GraphDisplay getDisplay() { return graphDisplay; } @Override public Size getDisplayArea() { Widget displayWidget = graphDisplay.asWidget(); int height = displayWidget.getOffsetHeight(); int width = displayWidget.getOffsetWidth(); return new DefaultSize(width, height); } @Override public String getName() { return "Graph"; } private Node getNode(VisualItem visualItem) { return visualItem.<NodeItem>getDisplayObject().getNode(); } private NodeItem[] getNodeItems() { LightweightCollection<VisualItem> visualItems = getVisualItems(); NodeItem[] nodeItems = new NodeItem[visualItems.size()]; int i = 0; for (VisualItem visualItem : visualItems) { nodeItems[i++] = visualItem.getDisplayObject(); } return nodeItems; } public Resource getResourceByUri(String value) { return nodeResources.getByUri(value); } public ResourceManager getResourceManager() { return resourceManager; } // TODO cleanup @Override public SidePanelSection[] getSidePanelSections() { List<ViewContentDisplayAction> actions = new ArrayList<ViewContentDisplayAction>(); actions.add(new GraphLayoutAction(GraphLayouts.CIRCLE_LAYOUT)); actions.add(new GraphLayoutAction(GraphLayouts.HORIZONTAL_TREE_LAYOUT)); actions.add(new GraphLayoutAction(GraphLayouts.VERTICAL_TREE_LAYOUT)); actions.add(new GraphLayoutAction(GraphLayouts.RADIAL_LAYOUT)); actions.add(new GraphLayoutAction(GraphLayouts.SPRING_LAYOUT)); actions.add(new GraphLayoutAction(GraphLayouts.INDENTED_TREE_LAYOUT)); actions.add(new GraphLayoutAction(GraphLayouts.GRID_LAYOUT_BY_NODE_ID)); actions.add(new GraphLayoutAction(GraphLayouts.GRID_LAYOUT_BY_NODE_TYPE)); actions.add(new GraphLayoutAction(GraphLayouts.GRID_LAYOUT_ALPHABETICAL)); actions.add(new GraphLayoutAction(GraphLayouts.GRID_LAYOUT_BY_ARC_COUNT)); actions.add(new GraphLayoutAction(GraphLayouts.HORIZONTAL_LAYOUT)); actions.add(new GraphLayoutAction(GraphLayouts.VERTICAL_LAYOUT)); actions.add(new GraphLayoutAction(GraphLayouts.FORCE_DIRECTED_LAYOUT)); VerticalPanel layoutPanel = new VerticalPanel(); for (final ViewContentDisplayAction action : actions) { Button w = new Button(action.getLabel()); w.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { action.execute(); } }); layoutPanel.add(w); } return new SidePanelSection[] { new SidePanelSection("Layouts", layoutPanel), }; } @Override public Slot[] getSlots() { return new Slot[] { NODE_LABEL_SLOT, NODE_BORDER_COLOR, NODE_BACKGROUND_COLOR }; } private VisualItem getVisualItem(Node node) { return getVisualItem(node.getId()); } private VisualItem getVisualItem(NodeEvent<?> event) { return getVisualItem(event.getNode()); } @Override public void init(VisualItemContainer container, ViewContentDisplayCallback callback) { super.init(container, callback); initStateChangeHandlers(); } private void initArcTypeContainers() { for (ArcType arcType : arcStyleProvider.getArcTypes()) { arcItemContainersByArcTypeID.put(arcType.getArcTypeID(), new ArcItemContainer(arcType, graphDisplay, this)); } } private void initNodeMenuItems() { for (Entry<String, List<NodeMenuEntry>> entry : registry.getNodeMenuEntriesByCategory()) { String category = entry.getKey(); for (NodeMenuEntry nodeMenuEntry : entry.getValue()) { registerNodeMenuItem(category, nodeMenuEntry.getLabel(), nodeMenuEntry.getExpander()); } } } private void initStateChangeHandlers() { graphDisplay.addGraphDisplayReadyHandler(new GraphDisplayReadyEventHandler() { @Override public void onWidgetReady(GraphDisplayReadyEvent event) { ready = true; GraphEventHandler handler = new GraphEventHandler(); graphDisplay.addEventHandler(NodeDragHandleMouseDownEvent.TYPE, handler); graphDisplay.addEventHandler(NodeMouseOverEvent.TYPE, handler); graphDisplay.addEventHandler(NodeMouseOutEvent.TYPE, handler); graphDisplay.addEventHandler(NodeMouseClickEvent.TYPE, handler); graphDisplay.addEventHandler(NodeDragEvent.TYPE, handler); graphDisplay.addEventHandler(MouseMoveEvent.getType(), handler); initNodeMenuItems(); } }); graphDisplay.addGraphDisplayLoadingFailureHandler(new GraphDisplayLoadingFailureEventHandler() { @Override public void onLoadingFailure(GraphDisplayLoadingFailureEvent event) { // TODO handle loading failures } }); } @Override public boolean isAdaptableTo(Class<?> clazz) { if (GraphLayoutSupport.class.equals(clazz)) { return true; } return super.isAdaptableTo(clazz); } @Override public boolean isReady() { return ready; } private void positionNode(Node node) { // FIXME positioning: FlexVis takes care of positioning nodes into empty // space except for first node - if the node is the first node, we put // it in the center // TODO improve interface to access all resources assert node != null; if (getVisualItems().size() > 1) { return; } Widget displayWidget = graphDisplay.asWidget(); if (displayWidget == null) { return; // for tests } int height = displayWidget.getOffsetHeight(); int width = displayWidget.getOffsetWidth(); graphDisplay.setLocation(node, new Point(width / 2, height / 2)); } private void registerNodeMenuItem(String category, String menuLabel, final GraphNodeExpander nodeExpander) { graphDisplay.addNodeMenuItemHandler(menuLabel, new NodeMenuItemClickedHandler() { @Override public void onNodeMenuItemClicked(Node node) { if (!ready) { return; } nodeExpander.expand(getVisualItem(node), expansionCallback); } }, category); } private void removeGraphNode(VisualItem visualItem) { assert visualItem != null; nodeResources.removeResourceSet(visualItem.getResources()); for (ArcItemContainer arcItemContainer : arcItemContainersByArcTypeID.values()) { arcItemContainer.removeVisualItem(visualItem); } graphDisplay.removeNode(getNode(visualItem)); } @Override public void restore(Memento state, PersistableRestorationService restorationService, ResourceSetAccessor accessor) { restoreArcItemContainers(restorationService, accessor, state.getChild(MEMENTO_ARC_ITEM_CONTAINERS_CHILD)); restoreNodeLocations(state.getChild(MEMENTO_NODE_LOCATIONS_CHILD)); } private void restoreArcItemContainers(PersistableRestorationService restorationService, ResourceSetAccessor accessor, Memento child) { for (Entry<String, Memento> entry : child.getChildren().entrySet()) { arcItemContainersByArcTypeID.get(entry.getKey()).restore(entry.getValue(), restorationService, accessor); } } private void restoreNodeLocations(Memento state) { for (VisualItem visualItem : getVisualItems()) { NodeItem item = visualItem.getDisplayObject(); Memento nodeMemento = state.getChild(visualItem.getId()); Point location = new Point((Integer) nodeMemento.getValue(MEMENTO_X), (Integer) nodeMemento.getValue(MEMENTO_Y)); setLocation(item, location); } } @Override public void runLayout(GraphLayout layout) { assert layout != null; layout.run(getNodeItems(), getArcItems(), this); } @Override public void runLayout(String layout) { assert layout != null; graphDisplay.runLayout(layout); } @Override public Memento save(ResourceSetCollector resourceSetCollector) { Memento result = new Memento(); result.addChild(MEMENTO_NODE_LOCATIONS_CHILD, saveNodeLocations()); result.addChild(MEMENTO_ARC_ITEM_CONTAINERS_CHILD, saveArcTypeContainers(resourceSetCollector)); return result; } private Memento saveArcTypeContainers(ResourceSetCollector resourceSetCollector) { Memento memento = new Memento(); for (Entry<String, ArcItemContainer> entry : arcItemContainersByArcTypeID.entrySet()) { memento.addChild(entry.getKey(), entry.getValue().save(resourceSetCollector)); } return memento; } private Memento saveNodeLocations() { Memento state = new Memento(); for (VisualItem visualItem : getVisualItems()) { NodeItem nodeItem = visualItem.getDisplayObject(); Point location = graphDisplay.getLocation(nodeItem.getNode()); Memento nodeMemento = new Memento(); nodeMemento.setValue(MEMENTO_X, location.getX()); nodeMemento.setValue(MEMENTO_Y, location.getY()); state.addChild(visualItem.getId(), nodeMemento); } return state; } /** * If the arc type becomes invisible, all arcs of this arcType from the view * and arcs of this arc type are not shown any more. If the arc types * becomes visible, all arcs of this type are added. */ // TODO expose arc type configurations and use listener mechanism public void setArcTypeVisible(String arcTypeId, boolean visible) { assert arcTypeId != null; assert arcItemContainersByArcTypeID.containsKey(arcTypeId); arcItemContainersByArcTypeID.get(arcTypeId).setVisible(visible); } @Override public void setAutomaticResources(ResourceSet automaticResources) { this.automaticResources = automaticResources; } @Override public void setLocation(NodeItem node, Point location) { graphDisplay.setLocation(node.getNode(), location); } @Override public void update(Delta<VisualItem> delta, LightweightCollection<Slot> updatedSlots) { for (VisualItem addedItem : delta.getAddedElements()) { createGraphNodeItem(addedItem); updateNode(addedItem); } updateArcsForVisuaItems(delta.getAddedElements()); for (VisualItem updatedItem : delta.getUpdatedElements()) { updateNode(updatedItem); } for (VisualItem visualItem : delta.getRemovedElements()) { removeGraphNode(visualItem); } if (!updatedSlots.isEmpty()) { for (VisualItem visualItem : getVisualItems()) { updateNode(visualItem); } } } public void updateArcsForResources(Iterable<Resource> resources) { updateArcsForVisuaItems(getVisualItems(resources)); } public void updateArcsForVisuaItems(LightweightCollection<VisualItem> visualItems) { assert visualItems != null; for (ArcItemContainer container : arcItemContainersByArcTypeID.values()) { container.update(visualItems); } } private void updateNode(VisualItem visualItem) { visualItem.<NodeItem>getDisplayObject().updateNode(); } }