/******************************************************************************* * Copyright (c) 2015, 2016 itemis AG and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Matthias Wienand (itemis AG) - initial API & implementation * *******************************************************************************/ package org.eclipse.gef.zest.fx.policies; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.eclipse.gef.fx.nodes.InfiniteCanvas; import org.eclipse.gef.geometry.convert.fx.FX2Geometry; import org.eclipse.gef.geometry.planar.Point; import org.eclipse.gef.graph.Graph; import org.eclipse.gef.mvc.fx.operations.ChangeViewportOperation; import org.eclipse.gef.mvc.fx.operations.ITransactionalOperation; import org.eclipse.gef.mvc.fx.parts.IVisualPart; import org.eclipse.gef.mvc.fx.parts.PartUtils; import org.eclipse.gef.mvc.fx.policies.ViewportPolicy; import org.eclipse.gef.mvc.fx.viewer.IViewer; import org.eclipse.gef.mvc.fx.viewer.InfiniteCanvasViewer; import org.eclipse.gef.zest.fx.models.NavigationModel; import org.eclipse.gef.zest.fx.operations.NavigateOperation; import org.eclipse.gef.zest.fx.parts.NodePart; import javafx.geometry.Bounds; import javafx.scene.Group; import javafx.scene.Node; /** * The {@link SemanticZoomPolicy} extends the {@link ViewportPolicy} for * associating semantic changes with viewport changes, i.e. opening of * nested/nesting graphs when the zoom level is changed below/above a certain * threshold. * * @author mwienand * */ public class SemanticZoomPolicy extends ViewportPolicy { private NavigationModel navigationModel; private IViewer viewer; @Override public ITransactionalOperation commit() { navigationModel = null; viewer = null; return super.commit(); } @Override protected ITransactionalOperation createOperation() { return new NavigateOperation(viewer); } /** * Returns a {@link List} containing all {@link NodePart}s (within the * currently rendered graph) that have a nested graph assigned to them. * * @return A {@link List} containing all {@link NodePart}s (within the * currently rendered graph) that have a nested graph assigned to * them. */ protected List<NodePart> findNestingNodes() { // find the first level visual parts (not considering nested graphs) List<IVisualPart<? extends Node>> rootChildren = getHost().getRoot().getChildrenUnmodifiable(); // rootChildren.get(0) should be the GraphPart containing the // NodeContentParts List<IVisualPart<? extends Node>> graphChildren = rootChildren.size() > 0 ? rootChildren.get(0).getChildrenUnmodifiable() : Collections.<IVisualPart<? extends Node>>emptyList(); // filter for NodePart List<NodePart> nestingNodeContentParts = PartUtils.filterParts(graphChildren, NodePart.class); // filter out all non-nesting nodes for (int i = nestingNodeContentParts.size() - 1; i >= 0; i--) { NodePart nodePart = nestingNodeContentParts.get(i); if (nodePart.getContent().getNestedGraph() == null) { nestingNodeContentParts.remove(i); } } return nestingNodeContentParts; } @Override protected ChangeViewportOperation getChangeViewportOperation() { return getNavigateOperation().getChangeViewportOperation(); } /** * Returns the {@link NavigateOperation} that is used to open nested/nesting * {@link Graph}s. * * @return The {@link NavigateOperation} that is used to open nested/nesting * {@link Graph}s. */ protected NavigateOperation getNavigateOperation() { return (NavigateOperation) getOperation(); } @Override public void init() { viewer = getHost().getRoot().getViewer(); navigationModel = viewer.getAdapter(NavigationModel.class); if (navigationModel == null) { throw new IllegalArgumentException("NavigationModel could not be obtained!"); } super.init(); } @Override public void zoom(boolean relative, boolean discretize, double relativeZoom, double sceneX, double sceneY) { // long startTimeNanos = System.nanoTime(); checkInitialized(); // determine initial and final zoom level double initialZoomLevel = getChangeViewportOperation().getNewContentTransform().getScaleX(); double finalZoomLevel = initialZoomLevel * relativeZoom; // open nested/nesting graph depending on zoom level boolean openGraph = false; if (initialZoomLevel < finalZoomLevel && finalZoomLevel > 3) { // zooming in => open nested graph (if any) // find all NodeContentParts with nested graphs List<NodePart> nestingNodeContentParts = findNestingNodes(); // determine the nesting node that is at least partially visible and // nearest to the pivot point double pivotDistance = Double.MAX_VALUE; NodePart pivotPart = null; InfiniteCanvas infiniteCanvas = ((InfiniteCanvasViewer) viewer).getCanvas(); org.eclipse.gef.geometry.planar.Rectangle viewportBounds = new org.eclipse.gef.geometry.planar.Rectangle(0, 0, infiniteCanvas.getWidth(), infiniteCanvas.getHeight()); Point pivotPoint = FX2Geometry.toPoint(infiniteCanvas.sceneToLocal(sceneX, sceneY)); for (NodePart nodePart : new ArrayList<>(nestingNodeContentParts)) { Group visual = nodePart.getVisual(); Bounds boundsInScene = visual.localToScene(visual.getLayoutBounds()); org.eclipse.gef.geometry.planar.Rectangle boundsInViewport = FX2Geometry .toRectangle(infiniteCanvas.sceneToLocal(boundsInScene)); if (boundsInViewport.touches(viewportBounds)) { double distance = boundsInViewport.getCenter().getDistance(pivotPoint); if (distance < pivotDistance) { pivotPart = nodePart; } } } // open nested graph if (pivotPart != null) { openGraph = true; getNavigateOperation().setFinalState(pivotPart.getContent().getNestedGraph(), true); } } else if (initialZoomLevel > finalZoomLevel && finalZoomLevel < 0.7) { // zooming out => open nesting graph (if any) final Graph currentGraph = (Graph) viewer.getContents().get(0); final Graph nestingGraph = currentGraph.getNestingNode() != null ? currentGraph.getNestingNode().getGraph() : null; if (nestingGraph != null) { openGraph = true; getNavigateOperation().setFinalState(nestingGraph, false); } } if (openGraph) { // execute the semantic zoom operations locallyExecuteOperation(); } else { // when no graph is opened, the viewport is regularly updated (the // super call executes the operation) super.zoom(true, true, relativeZoom, sceneX, sceneY); } // synchronize content children of nesting node parts // TODO: we need to extract the synchronization into a navigation // behavior, so it also works when undoing a navigate operation, or when // navigating graphs via OpenNestedGraphOnDoubleClickPolicy and // OpenParentGraphOnDoubleClickPolicy. for (NodePart nestingNodePart : findNestingNodes()) { nestingNodePart.refreshContentChildren(); nestingNodePart.refreshVisual(); } // System.out.println("zoom - " + (System.nanoTime() - startTimeNanos) / // 1000 / 1000 + "ms"); } }