/*
Copyright 2008-2013 Gephi
Authors : Mathieu Bastian <mathieu.bastian@gephi.org>
Website : http://www.gephi.org
This file is part of Gephi.
DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
Copyright 2013 Gephi Consortium. All rights reserved.
The contents of this file are subject to the terms of either the GNU
General Public License Version 3 only ("GPL") or the Common
Development and Distribution License("CDDL") (collectively, the
"License"). You may not use this file except in compliance with the
License. You can obtain a copy of the License at
http://gephi.org/about/legal/license-notice/
or /cddl-1.0.txt and /gpl-3.0.txt. See the License for the
specific language governing permissions and limitations under the
License. When distributing the software, include this License Header
Notice in each file and include the License files at
/cddl-1.0.txt and /gpl-3.0.txt. If applicable, add the following below the
License Header, with the fields enclosed by brackets [] replaced by
your own identifying information:
"Portions Copyrighted [year] [name of copyright owner]"
If you wish your version of this file to be governed by only the CDDL
or only the GPL Version 3, indicate your decision by adding
"[Contributor] elects to include this software in this distribution
under the [CDDL or GPL Version 3] license." If you do not indicate a
single choice of license, a recipient has the option to distribute
your version of this file under either the CDDL, the GPL Version 3 or
to extend the choice of license to its licensees as provided above.
However, if you add GPL Version 3 code and therefore, elected the GPL
Version 3 license, then the option applies only if the new code is
made subject to such option by the copyright holder.
Contributor(s):
Portions Copyrighted 2013 Gephi Consortium.
*/
package org.gephi.appearance;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.gephi.appearance.api.AppearanceModel;
import org.gephi.appearance.api.AttributeFunction;
import org.gephi.appearance.api.Function;
import org.gephi.appearance.api.Interpolator;
import org.gephi.appearance.api.Partition;
import org.gephi.appearance.spi.PartitionTransformer;
import org.gephi.appearance.spi.RankingTransformer;
import org.gephi.appearance.spi.SimpleTransformer;
import org.gephi.appearance.spi.Transformer;
import org.gephi.appearance.spi.TransformerUI;
import org.gephi.graph.api.AttributeUtils;
import org.gephi.graph.api.Column;
import org.gephi.graph.api.ColumnObserver;
import org.gephi.graph.api.DirectedGraph;
import org.gephi.graph.api.Edge;
import org.gephi.graph.api.Element;
import org.gephi.graph.api.ElementIterable;
import org.gephi.graph.api.Graph;
import org.gephi.graph.api.GraphController;
import org.gephi.graph.api.GraphModel;
import org.gephi.graph.api.GraphObserver;
import org.gephi.graph.api.Index;
import org.gephi.graph.api.Node;
import org.gephi.graph.api.Table;
import org.gephi.graph.api.types.TimeMap;
import org.gephi.project.api.Workspace;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
/**
*
* @author mbastian
*/
public class AppearanceModelImpl implements AppearanceModel {
private final Workspace workspace;
private final GraphModel graphModel;
private final Interpolator defaultInterpolator;
private boolean localScale = false;
// Transformers
private final List<Transformer> nodeTransformers;
private final List<Transformer> edgeTransformers;
// Transformer UIS
private final Map<Class, TransformerUI> transformerUIs;
//Functions
private final Object functionLock;
//
private final FunctionsModel functionsMain;
private final Map<Graph, FunctionsModel> functions = new HashMap<>();
public AppearanceModelImpl(Workspace workspace) {
this.workspace = workspace;
this.graphModel = Lookup.getDefault().lookup(GraphController.class).getGraphModel(workspace);
this.defaultInterpolator = Interpolator.LINEAR;
this.functionLock = new Object();
this.transformerUIs = initTransformerUIs();
this.nodeTransformers = initNodeTransformers();
this.edgeTransformers = initEdgeTransformers();
//Functions
functionsMain = new FunctionsModel(graphModel.getGraph());
refreshFunctions(graphModel.getGraph());
}
@Override
public Workspace getWorkspace() {
return workspace;
}
@Override
public boolean isLocalScale() {
return localScale;
}
@Override
public Function[] getNodeFunctions(Graph graph) {
return refreshFunctions(graph).getNodeFunctions();
}
@Override
public Function[] getEdgeFunctions(Graph graph) {
return refreshFunctions(graph).getEdgeFunctions();
}
@Override
public Function getNodeFunction(Graph graph, Column column, Class<? extends Transformer> transformer) {
for (Function f : refreshFunctions(graph).getNodeFunctions()) {
if (f.isAttribute() && f.getTransformer().getClass().equals(transformer)
&& ((AttributeFunction) f).getColumn().equals(column)) {
return f;
}
}
return null;
}
@Override
public Function getEdgeFunction(Graph graph, Column column, Class<? extends Transformer> transformer) {
for (Function f : refreshFunctions(graph).getEdgeFunctions()) {
if (f.isAttribute() && f.getTransformer().getClass().equals(transformer)
&& ((AttributeFunction) f).getColumn().equals(column)) {
return f;
}
}
return null;
}
@Override
public Function getNodeFunction(Graph graph, GraphFunction graphFunction, Class<? extends Transformer> transformer) {
String id = getNodeId(transformer, graphFunction);
for (Function f : refreshFunctions(graph).getNodeFunctions()) {
if (((FunctionImpl) f).getId().equals(id) && f.getTransformer().getClass().equals(transformer)) {
return f;
}
}
return null;
}
@Override
public Function getEdgeFunction(Graph graph, GraphFunction graphFunction, Class<? extends Transformer> transformer) {
String id = getEdgeId(transformer, graphFunction);
for (Function f : refreshFunctions(graph).getEdgeFunctions()) {
if (((FunctionImpl) f).getId().equals(id) && f.getTransformer().getClass().equals(transformer)) {
return f;
}
}
return null;
}
@Override
public Partition getNodePartition(Graph graph, Column column) {
synchronized (functionLock) {
refreshFunctions(graph);
FunctionsModel m;
if (graph.getView().isMainView()) {
m = functionsMain;
} else {
m = functions.get(graph);
}
if (m != null) {
return m.nodeFunctionsModel.getPartition(column);
}
return null;
}
}
@Override
public Partition getEdgePartition(Graph graph, Column column) {
synchronized (functionLock) {
refreshFunctions(graph);
FunctionsModel m;
if (graph.getView().isMainView()) {
m = functionsMain;
} else {
m = functions.get(graph);
}
if (m != null) {
return m.edgeFunctionsModel.getPartition(column);
}
return null;
}
}
private FunctionsModel refreshFunctions(Graph graph) {
synchronized (functionLock) {
FunctionsModel m;
if (graph.getView().isMainView()) {
m = functionsMain;
} else {
m = functions.get(graph);
if (m == null) {
m = new FunctionsModel(graph);
functions.put(graph, m);
}
}
//Check and detroy old
for (Iterator<Map.Entry<Graph, FunctionsModel>> it = functions.entrySet().iterator();
it.hasNext();) {
Map.Entry<Graph, FunctionsModel> entry = it.next();
if (entry.getKey().getView().isDestroyed()) {
it.remove();
}
}
return m;
}
}
private class NodeFunctionsModel extends ElementFunctionsModel<Node> {
public NodeFunctionsModel(Graph graph) {
super(graph);
}
@Override
public Class<? extends Element> getElementClass() {
return Node.class;
}
@Override
public Iterable<Node> getElements() {
return graph.getNodes();
}
@Override
public Table getTable() {
return graph.getModel().getNodeTable();
}
@Override
public Index<Node> getIndex(boolean localScale) {
return localScale ? graph.getModel().getNodeIndex(graph.getView()) : graph.getModel().getNodeIndex();
}
@Override
public List<Transformer> getTransformers() {
return nodeTransformers;
}
@Override
public String getIdPrefix() {
return "node";
}
@Override
public void refreshGraphFunctions() {
if (!rankings.containsKey(getIdStr(GraphFunction.NODE_DEGREE.getId()))) {
rankings.put(getIdStr(GraphFunction.NODE_DEGREE.getId()), new DegreeRankingImpl(graph));
}
if (graph.isDirected()) {
if (!rankings.containsKey(getIdStr(GraphFunction.NODE_INDEGREE.getId()))) {
DirectedGraph directedGraph = (DirectedGraph) graph;
rankings.put(getIdStr(GraphFunction.NODE_INDEGREE.getId()), new InDegreeRankingImpl(directedGraph));
rankings.put(getIdStr(GraphFunction.NODE_OUTDEGREE.getId()), new OutDegreeRankingImpl(directedGraph));
}
} else {
rankings.remove(getIdStr(GraphFunction.NODE_INDEGREE.getId()));
rankings.remove(getIdStr(GraphFunction.NODE_OUTDEGREE.getId()));
}
// Degree functions
for (Transformer t : getRankingTransformers()) {
String degreeId = getId(t, GraphFunction.NODE_DEGREE.getId());
RankingImpl degreeRanking = rankings.get(getIdStr(GraphFunction.NODE_DEGREE.getId()));
if (!graphFunctions.containsKey(degreeId)) {
String name = NbBundle.getMessage(AppearanceModelImpl.class, "NodeGraphFunction.Degree.name");
graphFunctions.put(degreeId, new GraphFunctionImpl(degreeId, name, Node.class, graph, t, getTransformerUI(t), degreeRanking, defaultInterpolator));
}
degreeRanking.refresh();
String indegreeId = getId(t, GraphFunction.NODE_INDEGREE.getId());
String outdegreeId = getId(t, GraphFunction.NODE_OUTDEGREE.getId());
RankingImpl indegreeRanking = rankings.get(getIdStr(GraphFunction.NODE_INDEGREE.getId()));
RankingImpl outdegreeRanking = rankings.get(getIdStr(GraphFunction.NODE_OUTDEGREE.getId()));
if (indegreeRanking != null && outdegreeRanking != null) {
if (!graphFunctions.containsKey(indegreeId)) {
String inDegreeName = NbBundle.getMessage(AppearanceModelImpl.class, "NodeGraphFunction.InDegree.name");
String outDegreeName = NbBundle.getMessage(AppearanceModelImpl.class, "NodeGraphFunction.OutDegree.name");
graphFunctions.put(indegreeId, new GraphFunctionImpl(indegreeId, inDegreeName, Node.class, graph, t, getTransformerUI(t), indegreeRanking, defaultInterpolator));
graphFunctions.put(outdegreeId, new GraphFunctionImpl(outdegreeId, outDegreeName, Node.class, graph, t, getTransformerUI(t), outdegreeRanking, defaultInterpolator));
}
indegreeRanking.refresh();
outdegreeRanking.refresh();
} else {
graphFunctions.remove(indegreeId);
graphFunctions.remove(outdegreeId);
}
}
}
}
private class EdgeFunctionsModel extends ElementFunctionsModel<Edge> {
public EdgeFunctionsModel(Graph graph) {
super(graph);
}
@Override
public Iterable<Edge> getElements() {
return graph.getEdges();
}
@Override
public Class<? extends Element> getElementClass() {
return Edge.class;
}
@Override
public Table getTable() {
return graph.getModel().getEdgeTable();
}
@Override
public Index<Edge> getIndex(boolean localScale) {
return localScale ? graph.getModel().getEdgeIndex(graph.getView()) : graph.getModel().getEdgeIndex();
}
@Override
public List<Transformer> getTransformers() {
return edgeTransformers;
}
@Override
public String getIdPrefix() {
return "edge";
}
@Override
public void refreshGraphFunctions() {
if (!rankings.containsKey(getIdStr(GraphFunction.EDGE_WEIGHT.getId()))) {
rankings.put(getIdStr(GraphFunction.EDGE_WEIGHT.getId()), new EdgeWeightRankingImpl(graph));
}
if (graph.getModel().isMultiGraph()) {
if (!partitions.containsKey(getIdStr(GraphFunction.EDGE_TYPE.getId()))) {
partitions.put(getIdStr(GraphFunction.EDGE_TYPE.getId()), new EdgeTypePartitionImpl(graph));
}
} else {
partitions.remove(getIdStr(GraphFunction.EDGE_TYPE.getId()));
}
// Weight function
for (Transformer t : getRankingTransformers()) {
String weightId = getId(t, GraphFunction.EDGE_WEIGHT.getId());
RankingImpl ranking = rankings.get(getIdStr(GraphFunction.EDGE_WEIGHT.getId()));
if (!graphFunctions.containsKey(weightId)) {
String name = NbBundle.getMessage(AppearanceModelImpl.class, "EdgeGraphFunction.Weight.name");
graphFunctions.put(weightId, new GraphFunctionImpl(weightId, name, Edge.class, graph, t, getTransformerUI(t), ranking, defaultInterpolator));
}
ranking.refresh();
}
// Type Function
for (Transformer t : getPartitionTransformers()) {
String typeId = getId(t, GraphFunction.EDGE_TYPE.getId());
PartitionImpl partition = partitions.get(getIdStr(GraphFunction.EDGE_TYPE.getId()));
if (partition != null) {
if (!graphFunctions.containsKey(typeId)) {
String name = NbBundle.getMessage(AppearanceModelImpl.class, "EdgeGraphFunction.Type.name");
graphFunctions.put(typeId, new GraphFunctionImpl(typeId, name, Edge.class, graph, t, getTransformerUI(t), partition));
}
partition.refresh();
} else {
graphFunctions.remove(typeId);
}
}
}
}
private class FunctionsModel {
protected final Graph graph;
protected final NodeFunctionsModel nodeFunctionsModel;
protected final EdgeFunctionsModel edgeFunctionsModel;
public FunctionsModel(Graph graph) {
this.graph = graph;
this.nodeFunctionsModel = new NodeFunctionsModel(graph);
this.edgeFunctionsModel = new EdgeFunctionsModel(graph);
}
public Function[] getNodeFunctions() {
return getFunctions(nodeFunctionsModel).toArray(new Function[0]);
}
public Function[] getEdgeFunctions() {
return getFunctions(edgeFunctionsModel).toArray(new Function[0]);
}
private List<Function> getFunctions(ElementFunctionsModel model) {
model.refreshFunctions();
List<Function> functions = new ArrayList<>();
functions.addAll(model.simpleFunctions.values());
functions.addAll(model.graphFunctions.values());
functions.addAll(model.attributeFunctions.values());
return functions;
}
}
private abstract class ElementFunctionsModel<T extends Element> {
protected final Graph graph;
protected final GraphObserver graphObserver;
protected final Map<Column, ColumnObserver> columnObservers;
protected final Map<String, SimpleFunctionImpl> simpleFunctions;
protected final Map<String, GraphFunctionImpl> graphFunctions;
protected final Map<String, AttributeFunctionImpl> attributeFunctions;
protected final Map<String, PartitionImpl> partitions;
protected final Map<String, RankingImpl> rankings;
protected ElementFunctionsModel(Graph graph) {
this.graph = graph;
simpleFunctions = new HashMap<>();
graphFunctions = new HashMap<>();
attributeFunctions = new HashMap<>();
columnObservers = new HashMap<>();
graphObserver = graph.getModel().createGraphObserver(graph, false);
partitions = new HashMap<>();
rankings = new HashMap<>();
// Init simple
initSimpleFunctions();
}
public abstract Iterable<T> getElements();
public abstract Table getTable();
public abstract Index<T> getIndex(boolean localScale);
public abstract List<Transformer> getTransformers();
public abstract String getIdPrefix();
public abstract void refreshGraphFunctions();
public abstract Class<? extends Element> getElementClass();
public Partition getPartition(Column column) {
return partitions.get(getIdCol(column));
}
protected void refreshFunctions() {
graph.readLock();
try {
boolean graphHasChanged = graphObserver.isNew() || graphObserver.hasGraphChanged();
if (graphHasChanged) {
if (graphObserver.isNew()) {
graphObserver.hasGraphChanged();
}
refreshGraphFunctions();
}
refreshAttributeFunctions(graphHasChanged);
} finally {
graph.readUnlock();
}
}
private void refreshAttributeFunctions(boolean graphHasChanged) {
Set<Column> columns = new HashSet<>();
for (Column column : getTable()) {
if (!column.isProperty()) {
columns.add(column);
}
}
//Clean
for (Iterator<Map.Entry<Column, ColumnObserver>> itr = columnObservers.entrySet().iterator(); itr.hasNext();) {
Map.Entry<Column, ColumnObserver> entry = itr.next();
if (!columns.contains(entry.getKey())) {
rankings.remove(getIdCol(entry.getKey()));
partitions.remove(getIdCol(entry.getKey()));
for (Transformer t : getTransformers()) {
attributeFunctions.remove(getId(t, entry.getKey()));
}
itr.remove();
if (!entry.getValue().isDestroyed()) {
entry.getValue().destroy();
}
}
}
//Get columns to be refreshed
Set<Column> toRefreshColumns = new HashSet<>();
for (Column column : columns) {
if (!columnObservers.containsKey(column)) {
columnObservers.put(column, column.createColumnObserver(false));
toRefreshColumns.add(column);
} else if (columnObservers.get(column).hasColumnChanged() || graphHasChanged) {
toRefreshColumns.add(column);
}
}
//Refresh ranking and partitions
for (Column column : toRefreshColumns) {
RankingImpl ranking = rankings.get(getIdCol(column));
PartitionImpl partition = partitions.get(getIdCol(column));
if (ranking == null && partition == null) {
if (isPartition(graph, column)) {
if (column.isIndexed()) {
partition = new AttributePartitionImpl(column, getIndex(false));
} else {
partition = new AttributePartitionImpl(column, graph);
}
partitions.put(getIdCol(column), partition);
}
if (isRanking(graph, column)) {
if (column.isIndexed()) {
ranking = new AttributeRankingImpl(column, graph, getIndex(localScale));
} else {
ranking = new AttributeRankingImpl(column, graph, null);
}
rankings.put(getIdCol(column), ranking);
}
}
if (ranking != null) {
ranking.refresh();
}
if (partition != null) {
partition.refresh();
}
}
//Ranking functions
for (Transformer t : getRankingTransformers()) {
for (Column col : toRefreshColumns) {
RankingImpl ranking = rankings.get(getIdCol(col));
if (ranking != null) {
String id = getId(t, col);
if (!attributeFunctions.containsKey(id)) {
attributeFunctions.put(id, new AttributeFunctionImpl(id, graph, col, t, getTransformerUI(t), ranking, defaultInterpolator));
}
}
}
}
//Partition functions
for (Transformer t : getPartitionTransformers()) {
for (Column col : toRefreshColumns) {
PartitionImpl partition = partitions.get(getIdCol(col));
if (partition != null) {
String id = getId(t, col);
if (!attributeFunctions.containsKey(id)) {
attributeFunctions.put(id, new AttributeFunctionImpl(id, graph, col, t, getTransformerUI(t), partition));
}
}
}
}
}
private void initSimpleFunctions() {
for (Transformer transformer : getTransformers()) {
if (transformer instanceof SimpleTransformer) {
String id = getId(transformer, "simple");
simpleFunctions.put(id, new SimpleFunctionImpl(id, getElementClass(), graph, transformer, getTransformerUI(transformer)));
}
}
}
protected TransformerUI getTransformerUI(Transformer transformer) {
return transformerUIs.get(transformer.getClass());
}
protected List<Transformer> getRankingTransformers() {
List<Transformer> res = new ArrayList<>();
for (Transformer t : getTransformers()) {
if (t instanceof RankingTransformer) {
res.add(t);
}
}
return res;
}
protected List<Transformer> getPartitionTransformers() {
List<Transformer> res = new ArrayList<>();
for (Transformer t : getTransformers()) {
if (t instanceof PartitionTransformer) {
res.add(t);
}
}
return res;
}
protected String getId(Transformer transformer, Column column) {
return getIdPrefix() + "_" + transformer.getClass().getSimpleName() + "_column_" + column.getId();
}
protected String getId(Transformer transformer, String suffix) {
return getIdPrefix() + "_" + transformer.getClass().getSimpleName() + "_" + suffix;
}
protected String getIdStr(String suffix) {
return getIdPrefix() + "_" + suffix;
}
}
protected String getIdCol(Column column) {
return (AttributeUtils.isNodeColumn(column) ? "node" : "edge") + "_column_" + column.getId();
}
protected String getNodeId(Class<? extends Transformer> transformer, GraphFunction graphFunction) {
return "node_" + transformer.getSimpleName() + "_" + graphFunction.getId();
}
protected String getEdgeId(Class<? extends Transformer> transformer, GraphFunction graphFunction) {
return "edge_" + transformer.getSimpleName() + "_" + graphFunction.getId();
}
private boolean isPartition(Graph graph, Column column) {
int valueCount, elementCount;
if (column.isDynamic()) {
if (!column.isNumber()) {
return true;
}
Set<Object> set = new HashSet<>();
boolean hasNullValue = false;
int elements = 0;
ElementIterable<? extends Element> iterable = AttributeUtils.isNodeColumn(column) ? graph.getNodes() : graph.getEdges();
for (Element el : iterable) {
TimeMap val = (TimeMap) el.getAttribute(column);
if (val != null) {
Object[] va = val.toValuesArray();
for (Object v : va) {
if (v != null) {
set.add(v);
} else {
hasNullValue = true;
}
elements++;
}
}
}
valueCount = set.size();
elementCount = elements;
} else if (column.isIndexed()) {
if (!column.isNumber()) {
return true;
}
Index index;
if (AttributeUtils.isNodeColumn(column)) {
index = graphModel.getNodeIndex(graph.getView());
} else {
index = graphModel.getEdgeIndex(graph.getView());
}
valueCount = index.countValues(column);
elementCount = index.countElements(column);
} else {
return false;
}
double ratio = valueCount / (double) elementCount;
return ratio <= 0.5 || (valueCount <= 100 && valueCount != elementCount);
}
private boolean isRanking(Graph graph, Column column) {
if (column.isDynamic() && column.isNumber()) {
ElementIterable<? extends Element> iterable = AttributeUtils.isNodeColumn(column) ? graph.getNodes() : graph.getEdges();
for (Element el : iterable) {
if (el.getAttribute(column, graph.getView()) != null) {
iterable.doBreak();
return true;
}
}
} else if (!column.isDynamic() && !column.isArray() && column.isIndexed() && column.isNumber()) {
Index index;
if (AttributeUtils.isNodeColumn(column)) {
index = localScale ? graphModel.getNodeIndex(graph.getView()) : graphModel.getNodeIndex();
} else {
index = localScale ? graphModel.getEdgeIndex(graph.getView()) : graphModel.getEdgeIndex();
}
if (index.countValues(column) > 0) {
return true;
}
}
return false;
}
public void setLocalScale(boolean localScale) {
this.localScale = localScale;
}
protected GraphModel getGraphModel() {
return graphModel;
}
private Map<Class, TransformerUI> initTransformerUIs() {
//Index UIs
Map<Class, TransformerUI> uis = new HashMap<>();
for (TransformerUI ui : Lookup.getDefault().lookupAll(TransformerUI.class)) {
Class transformerClass = ui.getTransformerClass();
if (transformerClass == null) {
throw new NullPointerException("Transformer class can' be null");
}
if (uis.containsKey(transformerClass)) {
throw new RuntimeException("A Transformer can't be attach to multiple TransformerUI");
}
uis.put(transformerClass, ui);
}
return uis;
}
private List<Transformer> initNodeTransformers() {
List<Transformer> res = new ArrayList<>();
for (Transformer transformer : Lookup.getDefault().lookupAll(Transformer.class)) {
if (transformer.isNode()) {
res.add(transformer);
}
}
return res;
}
private List<Transformer> initEdgeTransformers() {
List<Transformer> res = new ArrayList<>();
for (Transformer transformer : Lookup.getDefault().lookupAll(Transformer.class)) {
if (transformer.isEdge()) {
res.add(transformer);
}
}
return res;
}
}