com.googlecode.blaisemath.graph.layout.GraphLayoutManager Maven / Gradle / Ivy
/*
* GraphLayoutManager.java
* Created Jan 29, 2011
*/
package com.googlecode.blaisemath.graph.layout;
/*
* #%L
* BlaiseGraphTheory
* --
* Copyright (C) 2009 - 2018 Elisha Peterson
* --
* 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.
* #L%
*/
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.Sets;
import com.googlecode.blaisemath.graph.Graph;
import com.googlecode.blaisemath.graph.GraphUtils;
import com.googlecode.blaisemath.graph.IterativeGraphLayout;
import com.googlecode.blaisemath.graph.StaticGraphLayout;
import com.googlecode.blaisemath.graph.mod.layout.CircleLayout;
import com.googlecode.blaisemath.graph.mod.layout.CircleLayout.CircleLayoutParameters;
import com.googlecode.blaisemath.graph.mod.layout.PositionalAddingLayout;
import com.googlecode.blaisemath.util.coordinate.CoordinateManager;
import java.awt.geom.Point2D;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
/**
*
* Manages graph layout within a background thread, in situations where the graph
* or node locations might be simultaneously modified from other threads.
* Executes a graph layout algorithm in a background thread. Uses an
* {@link IterativeGraphLayout} algorithm, whose results are supplied to the
* {@link CoordinateManager}. This class is not thread-safe, so all of its
* methods should be accessed from a single thread. However, coordinate locations can
* be accessed or updated in the {@code CoordinateManager} from any thread, since
* that class is thread-safe.
*
*
* @param type of node in graph
* @author elisha
*/
@NotThreadSafe
public final class GraphLayoutManager {
private static final Logger LOG = Logger.getLogger(GraphLayoutManager.class.getName());
//
static final int NODE_CACHE_SIZE = 20000;
/** Graph property */
public static final String P_GRAPH = "graph";
/** Layout property */
public static final String P_LAYOUT = "layoutAlgorithm";
/** Whether layout is active */
public static final String P_LAYOUT_ACTIVE = "layoutTaskActive";
//
/** Graph */
private Graph graph;
/** Locates nodes in the graph */
private final CoordinateManager coordManager = CoordinateManager.create(NODE_CACHE_SIZE);
/** The initial layout scheme */
private final StaticGraphLayout initialLayout = CircleLayout.getInstance();
/** The initial layout parameters */
private final CircleLayoutParameters initialLayoutParameters = new CircleLayoutParameters(50);
/** The layout scheme for adding vertices */
private final StaticGraphLayout addingLayout = new PositionalAddingLayout();
/** The initial layout parameters */
private final CircleLayoutParameters addingLayoutParameters = new CircleLayoutParameters(100);
/** Manager for iterative graph layout algorithm */
private final IterativeGraphLayoutManager iterativeLayoutManager = new IterativeGraphLayoutManager();
/** Service that manages iterative graph layout on background thread. */
private IterativeGraphLayoutService iterativeLayoutService;
/** Handles property change events */
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
//
/** Initializes with an empty graph */
public GraphLayoutManager() {
iterativeLayoutManager.setCoordinateManager(coordManager);
setGraph(GraphUtils.emptyGraph());
}
/**
* Initializes with a given graph.
* @param graph graph for layout
* @return manager instance
*/
public static GraphLayoutManager create(Graph graph) {
GraphLayoutManager res = new GraphLayoutManager();
res.setGraph(graph);
return res;
}
//
//
//
// PROPERTIES
//
/**
* Object used to map locations of points.
* @return point manager
*/
public CoordinateManager getCoordinateManager() {
return coordManager;
}
/**
* Returns copy of the locations of objects in the graph.
* @return locations, as a copy of the map provided in the point manager
*/
public Map getNodeLocationCopy() {
return coordManager.getActiveLocationCopy();
}
/**
* Return the graph
* @return the layout manager's graph
*/
public Graph getGraph() {
return graph;
}
/**
* Changes the graph. Uses the default initial position layout to position
* nodes if the current graph was null, otherwise uses the adding layout for
* any nodes that do not have current positions.
*
* @param g the graph
*/
public void setGraph(Graph g) {
checkNotNull(g);
Graph old = this.graph;
if (old != g) {
boolean active = isLayoutTaskActive();
setLayoutTaskActive(false);
this.graph = g;
iterativeLayoutManager.setGraph(g);
initializeNodeLocations(old, g);
setLayoutTaskActive(active);
pcs.firePropertyChange(P_GRAPH, old, g);
}
}
/**
* Get layout algorithm
* @return current iterative layout algorithm
*/
@Nullable
public IterativeGraphLayout getLayoutAlgorithm() {
return iterativeLayoutManager.getLayout();
}
/**
* Sets up with an iterative graph layout. Cancels any ongoing layout, and does
* not start a new one.
* @param layout the layout algorithmut getLayoutAlgorithm() {
*/
public void setLayoutAlgorithm(@Nullable IterativeGraphLayout layout) {
Object old = iterativeLayoutManager.getLayout();
if (layout != old) {
setLayoutTaskActive(false);
iterativeLayoutService = new IterativeGraphLayoutService(iterativeLayoutManager);
iterativeLayoutManager.setLayout(layout);
pcs.firePropertyChange(P_LAYOUT, old, layout);
}
}
/**
* Get parameters associated with the current layout.
* @return parameters
*/
public Object getLayoutParameters() {
return iterativeLayoutManager.getParameters();
}
/**
* Set parameters for the current layout
* @param params new parameters
*/
public void setLayoutParameters(Object params) {
iterativeLayoutManager.setParameters(params);
}
/**
* Return whether layout task is currently active.
* @return true if an iterative layout is active
*/
public boolean isLayoutTaskActive() {
return iterativeLayoutService != null &&
iterativeLayoutService.isLayoutActive();
}
/**
* Use to change the status of the layout task, either starting or stopping it.
* @param on true to animate, false to stop animating
*/
public void setLayoutTaskActive(boolean on) {
boolean old = isLayoutTaskActive();
if (on == old) {
return;
} else if (on) {
stopLayoutTaskNow();
iterativeLayoutService = new IterativeGraphLayoutService(iterativeLayoutManager);
iterativeLayoutService.startAsync();
} else {
stopLayoutTaskNow();
}
pcs.firePropertyChange(P_LAYOUT_ACTIVE, !on, on);
}
/**
* Stops the layout timer
*/
private void stopLayoutTaskNow() {
if (iterativeLayoutService != null) {
iterativeLayoutService.stopAsync();
iterativeLayoutManager.reset();
try {
iterativeLayoutService.awaitTerminated(100, TimeUnit.MILLISECONDS);
} catch (TimeoutException ex) {
LOG.log(Level.WARNING, "Layout service was not terminated", ex);
}
}
}
//
/**
* When the graph is changes, call this method to set up initial positions
* for nodes in the graph. Will attempt to use cached nodes if possible.
* Otherwise, it may execute the "initial layout" algorithm or the "adding
* layout" algorithm.
*
* @todo may take some time to execute if the graph is large, consider improving
* this class's design by running the initial layout in a background thread;
* also, locking on the CM may be problematic if the layout takes a long time
*/
private void initializeNodeLocations(Graph old, Graph g) {
synchronized (coordManager) {
Set oldNodes = Sets.difference(coordManager.getActive(), g.nodes());
coordManager.deactivate(oldNodes);
// defer to existing locations if possible
if (coordManager.locatesAll(g.nodes())) {
coordManager.reactivate(g.nodes());
} else {
// lays out new graph entirely
Map newLoc;
if (old == null) {
newLoc = initialLayout.layout(g, null, initialLayoutParameters);
} else {
Map curLocs = coordManager.getActiveLocationCopy();
newLoc = addingLayout.layout(g, curLocs, addingLayoutParameters);
}
// remove objects that are already in coordinate manager
newLoc.keySet().removeAll(coordManager.getActive());
newLoc.keySet().removeAll(coordManager.getInactive());
coordManager.reactivate(g.nodes());
coordManager.putAll(newLoc);
}
// log size mismatches to help with debugging
int sz = coordManager.getActive().size();
boolean check = sz == g.nodeCount();
if (!check) {
LOG.log(Level.WARNING,
"Object sizes don''t match: {0} locations, but {1} nodes!",
new Object[]{sz, g.nodeCount()});
}
}
}
//
//
// MUTATORS
//
/**
* Update the locations of the specified nodes with the specified values.
* If an iterative layout is currently active, locations are updated at the
* layout. Otherwise, locations are updated by the point manager. Nodes that are
* in the graph but whose positions are not in the provided map will not be moved.
*
* @param nodePositions new locations for objects
*/
public void requestLocations(Map nodePositions) {
checkNotNull(nodePositions);
if (isLayoutTaskActive()) {
iterativeLayoutManager.requestPositions(nodePositions, false);
} else {
coordManager.putAll(nodePositions);
}
}
/**
* Update positions of current using specified layout algorithm. This method will
* replace the coordinates of objects in the graph.
* @param layout static layout algorithm
* @param ic initial conditionsn fo rstatic layout algorithm
* @param parameters layout parameters
* @param parameters type
*/
public
void applyLayout(StaticGraphLayout
layout, Map ic,
P parameters){
requestLocations(layout.layout(graph, ic, parameters));
}
/**
* Manually iterate layout, if an iterative layout has been provided.
*/
public void iterateLayout() {
try {
iterativeLayoutService.runOneIteration();
} catch (Exception ex) {
LOG.log(Level.SEVERE, "Error iterating layout", ex);
}
}
//
//
//
// LAYOUT TASK
//
//
//
//
// EVENT HANDLING
//
public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
pcs.removePropertyChangeListener(propertyName, listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
pcs.addPropertyChangeListener(propertyName, listener);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
//
}