jflowmap.FlowMapGraph.java Source code

Java tutorial

Introduction

Here is the source code for jflowmap.FlowMapGraph.java

Source

/*
 * This file is part of JFlowMap.
 *
 * Copyright 2009 Ilya Boyandin
 *
 * 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 jflowmap;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jflowmap.data.AttrDataTypes;
import jflowmap.data.FlowMapGraphBuilder;
import jflowmap.data.FlowMapNodeTotals;
import jflowmap.data.FlowMapStats;
import jflowmap.data.GraphMLDatasetSpec;
import jflowmap.data.MultiFlowMapStats;
import jflowmap.data.SeqStat;
import jflowmap.data.StaxGraphMLReader;
import jflowmap.geom.GeomUtils;
import jflowmap.geom.Point;
import jflowmap.util.MathUtils;
import jflowmap.util.Tables;

import org.apache.log4j.Logger;

import prefuse.data.Edge;
import prefuse.data.Graph;
import prefuse.data.Node;
import prefuse.data.Table;
import prefuse.data.Tuple;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;

/**
 * @author Ilya Boyandin
 */

public class FlowMapGraph {

    //  private static final String EDGE_GROUPING_COLUMN = "_GROUPING";

    private static Logger logger = Logger.getLogger(FlowMapGraph.class);

    public static final Class<Double> WEIGHT_COLUMNS_DATA_TYPE = double.class;
    public static final String GRAPH_CLIENT_PROPERTY__ID = "id";
    public static final String GRAPH_NODE_ID_COLUMN = "_node_id";
    public static final String GRAPH_EDGE_SOURCE_NODE_COLUMN = Graph.DEFAULT_SOURCE_KEY;
    public static final String GRAPH_EDGE_TARGET_NODE_COLUMN = Graph.DEFAULT_TARGET_KEY;

    private static final String SUBDIVISION_POINTS_ATTR_NAME = "_subdivp";

    public static final String SRC = GRAPH_EDGE_SOURCE_NODE_COLUMN;
    public static final String TRG = GRAPH_EDGE_TARGET_NODE_COLUMN;
    public static final String SRC_TEMP_ID = SRC + "_id";
    public static final String TRG_TEMP_ID = TRG + "_id";

    private final Graph graph;
    private final FlowMapAttrSpec attrSpec;
    private final FlowMapStats stats;

    public FlowMapGraph(Graph graph, FlowMapAttrSpec attrSpec) {
        this(graph, attrSpec, null);
    }

    /**
     * This constructor is intended to be used when the stats have to be
     * induced and not calculated (for instance, in case when a global mapping over
     * a number of flow maps for small multiples must be used).
     * Otherwise, use {@link #FlowMapGraph(Graph, FlowMapAttrSpec)}.
     */
    public FlowMapGraph(Graph graph, FlowMapAttrSpec attrSpec, FlowMapStats stats) {
        attrSpec.checkValidityFor(graph);
        this.graph = graph;
        this.attrSpec = attrSpec;
        //    List<String> weightAttrs = Lists.newArrayList(attrSpec.getEdgeWeightAttrNames());
        //    Collections.sort(weightAttrs);

        List<String> weightAttrs = attrSpec.getFlowWeightAttrs();
        if (weightAttrs.size() == 0) {
            throw new IllegalArgumentException("FlowMapGraph must have at least one weight attr. "
                    + "Available columns: " + Iterables.toString(Tables.columns(graph.getEdgeTable())));
        }

        logger.info("Creating FlowMapGraph '" + getGraphId(graph) + "' with " + graph.getNodeTable().getRowCount()
                + " nodes" + ", " + graph.getEdgeTable().getRowCount() + " flows" + ", and " + weightAttrs.size()
                + " flow weight attrs");

        if (logger.isDebugEnabled()) {
            logger.debug("FlowMapGraph flow weight attrs: " + weightAttrs);
        }

        if (stats == null) {
            //stats = EdgeListFlowMapStats.createFor(edges(), attrSpec);
            stats = MultiFlowMapStats.createFor(this);
            logger.info("Flow weight stats: " + stats.getEdgeWeightStats());
        }
        this.stats = stats;
    }

    /**
     * Note: the iterators are not guaranteed to be fail-safe
     */
    public Iterable<Node> nodes() {
        return new Iterable<Node>() {
            @SuppressWarnings("unchecked")
            @Override
            public Iterator<Node> iterator() {
                return getGraph().nodes();
            }
        };
    }

    public Iterable<Node> nodesHavingEdges(final FlowDirection dir) {
        return Iterables.filter(nodes(), new Predicate<Node>() {
            @Override
            public boolean apply(Node node) {
                return dir.degreeOf(node) > 0;
            }
        });
    }

    /**
     * Note: the iterators are not guaranteed to be fail-safe
     */
    public Iterable<Edge> edges() {
        return new Iterable<Edge>() {
            @SuppressWarnings("unchecked")
            @Override
            public Iterator<Edge> iterator() {
                return getGraph().edges();
            }
        };
    }

    //  /**
    //   * Returns all egdes which constitute the grouping with the given groupingName.
    //   * @param groupingName If null, returns edges which are not in any grouping
    //   */
    //  public Iterable<Edge> edges(final String groupingName) {
    //    return Iterables.filter(edges(), new Predicate<Edge>() {
    //      @Override
    //      public boolean apply(Edge e) {
    //        String group = getEdgeGroupingName(e);
    //        if (groupingName == null) {
    //          return group == null;
    //        } else {
    //          return groupingName.equals(group);
    //        }
    //      }
    //    });
    //  }

    public String getId() {
        return getGraphId(graph);
    }

    public String getNodeLonAttr() {
        return attrSpec.getNodeLonAttr();
    }

    public String getNodeLatAttr() {
        return attrSpec.getNodeLatAttr();
    }

    /**
     * @return An immutable list which can thus be reused without defensive copying.
     */
    public List<String> getEdgeWeightAttrs() {
        return attrSpec.getFlowWeightAttrs();
    }

    public int getEdgeWeightAttrsCount() {
        return getEdgeWeightAttrs().size();
    }

    public String getNodeLabelAttr() {
        return attrSpec.getNodeLabelAttr();
    }

    public FlowMapStats getStats() {
        return stats;
    }

    public FlowMapAttrSpec getAttrSpec() {
        return attrSpec;
    }

    public Graph getGraph() {
        return graph;
    }

    public static String getGraphId(Graph graph) {
        return (String) graph.getClientProperty(GRAPH_CLIENT_PROPERTY__ID);
    }

    public String getGraphId() {
        return getGraphId(graph);
    }

    public void setGraphId(String id) {
        setGraphId(graph, id);
    }

    public static void setGraphId(Graph graph, String id) {
        graph.putClientProperty(GRAPH_CLIENT_PROPERTY__ID, id);
    }

    public String getNodeId(Node node) {
        return getIdOfNode(node);
    }

    public static String getIdOfNode(Node node) {
        return node.getString(GRAPH_NODE_ID_COLUMN);
    }

    public String getNodeLabel(Node node) {
        return node.getString(attrSpec.getNodeLabelAttr());
    }

    public Node getNodeOf(Edge edge, FlowEndpoint pos) {
        switch (pos) {
        case ORIGIN:
            return edge.getSourceNode();
        case DEST:
            return edge.getTargetNode();
        }
        throw new AssertionError();
    }

    public static Node findNodeById(Graph graph, String nodeId) {
        int index = findNodeIndexById(graph, nodeId);
        if (index >= 0) {
            return graph.getNode(index);
        }
        return null;
    }

    public static Iterable<String> listFlowAttrs(Graph graph) {
        List<String> attrs = Lists.newArrayList();
        Table et = graph.getEdgeTable();
        for (int i = 0; i < et.getColumnCount(); i++) {
            String cname = et.getColumnName(i);
            if (!cname.equals(Graph.DEFAULT_SOURCE_KEY) && !cname.equals(Graph.DEFAULT_TARGET_KEY)) {
                attrs.add(cname);
            }
        }
        return attrs;
    }

    //  public static List<String> findEdgeAttrsByPattern(Graph graph, String pattern) {
    //    Pattern re = Pattern.compile(pattern);
    //    Table et = graph.getEdgeTable();
    //    List<String> attrs = Lists.newArrayList();
    //    for (int i = 0; i < et.getColumnCount(); i++) {
    //      String cname = et.getColumnName(i);
    //      if (re.matcher(cname).matches()) {
    //        if (!cname.equals(Graph.DEFAULT_SOURCE_KEY) &&
    //            !cname.equals(Graph.DEFAULT_TARGET_KEY)) {
    //          attrs.add(cname);
    //        }
    //      }
    //    }
    //    return attrs;
    //  }

    public static int findNodeIndexById(Graph graph, String nodeId) {
        for (int i = 0, len = graph.getNodeCount(); i < len; i++) {
            Node node = graph.getNode(i);
            if (nodeId.equals(getIdOfNode(node))) {
                return i;
            }
        }
        return -1;
    }

    //  public static <T> Set<T> getNodeAttrValues(Graph graph, String attrName) {
    //    return getNodeAttrValues(Arrays.asList(graph), attrName);
    //  }

    public SeqStat getEdgeLengthStats() {
        return stats.getEdgeLengthStats();
    }

    public double getEdgeWeight(Edge edge, String weightAttr) {
        return edge.getDouble(weightAttr);
    }

    public Iterable<Double> getEdgeWeights(final Edge edge) {
        return Iterables.transform(getEdgeWeightAttrs(), new Function<String, Double>() {
            @Override
            public Double apply(String weightAttr) {
                return getEdgeWeight(edge, weightAttr);
            }
        });
    }

    public List<Point> getEdgePoints(Edge edge) {
        List<Point> subdiv;
        if (hasEdgeSubdivisionPoints(edge)) {
            subdiv = getEdgeSubdivisionPoints(edge);
        } else {
            subdiv = Collections.emptyList();
        }
        List<Point> points = Lists.newArrayListWithExpectedSize(subdiv.size() + 2);
        points.add(getEdgeSourcePoint(edge));
        points.addAll(subdiv);
        points.add(getEdgeTargetPoint(edge));
        return points;
    }

    public boolean isSelfLoop(Edge edge) {
        Node src = edge.getSourceNode();
        Node target = edge.getTargetNode();
        if (src == target) {
            return true;
        }
        return GeomUtils.isSelfLoopEdge(src.getDouble(attrSpec.getNodeLonAttr()),
                target.getDouble(attrSpec.getNodeLonAttr()), src.getDouble(attrSpec.getNodeLatAttr()),
                target.getDouble(attrSpec.getNodeLatAttr()));
    }

    public boolean hasEdgeSubdivisionPoints(Edge edge) {
        return edge.canGet(SUBDIVISION_POINTS_ATTR_NAME, List.class) &&
        // the above will return true after calling removeAllEdgeSubdivisionPoints(),
        // so we need to add the following null check:
                (edge.get(SUBDIVISION_POINTS_ATTR_NAME) != null);
    }

    @SuppressWarnings("unchecked")
    public List<Point> getEdgeSubdivisionPoints(Edge edge) {
        checkContainsEdge(edge);
        return (List<Point>) edge.get(SUBDIVISION_POINTS_ATTR_NAME);
    }

    public void setEdgeSubdivisionPoints(Edge edge, List<Point> points) {
        checkContainsEdge(edge);
        if (!graph.hasSet(SUBDIVISION_POINTS_ATTR_NAME)) {
            graph.addColumn(SUBDIVISION_POINTS_ATTR_NAME, List.class);
        }
        edge.set(SUBDIVISION_POINTS_ATTR_NAME, points);
    }

    public void removeAllEdgeSubdivisionPoints() {
        int numEdges = graph.getEdgeCount();
        for (int i = 0; i < numEdges; i++) {
            Edge edge = graph.getEdge(i);
            if (hasEdgeSubdivisionPoints(edge)) {
                edge.set(SUBDIVISION_POINTS_ATTR_NAME, null);
            }
        }
    }

    private void checkContainsEdge(Edge edge) {
        if (!graph.containsTuple(edge)) {
            throw new IllegalArgumentException("Edge is not in graph");
        }
    }

    public Point getEdgeSourcePoint(Edge edge) {
        Node src = edge.getSourceNode();
        return new Point(src.getDouble(attrSpec.getNodeLonAttr()), src.getDouble(attrSpec.getNodeLatAttr()));
    }

    public Point getEdgeTargetPoint(Edge edge) {
        Node target = edge.getTargetNode();
        return new Point(target.getDouble(attrSpec.getNodeLonAttr()), target.getDouble(attrSpec.getNodeLatAttr()));
    }

    @SuppressWarnings("unchecked")
    private <T> Set<T> nodeAttrValues(String attrName) {
        Set<T> values = Sets.newLinkedHashSet();
        for (int i = 0, len = graph.getNodeCount(); i < len; i++) {
            Node node = graph.getNode(i);
            T v = (T) node.get(attrName);
            if (v != null) {
                values.add(v);
            }
        }
        return values;
    }

    /**
     * Creates a new FlowMapGraph in which edges of this FlowMapGraph
     * having the same value of {@nodeAttrToGroupBy} are grouped together.
     */
    //  public FlowMapGraph groupEdgesBy(Function<Edge, Object> groupFunction) {

    //  .withCumulativeEdges()

    //    String strv = v.toString();
    //    Node node = builder.addNode(strv, new Point(0, 0), strv);
    //    valueToEdge.put(v, node);

    /*
     Requirements:
     - Grouping by src/target nodes
     - Grouping by an src/target node attr
     - References from group nodes/edges to the ones in the group
     - Hierarchy of groupings (is an ordered list enough?)
     - Possibility to obtain edge weight stats for groupings (and merge them with stats of groupings
       on other levels).
        
     Solution:
     1.Create new FlowMapGraph for each grouping
     or 2. a dedicated EdgeGrouping class
        
        
     Heatmap drawing code will have to support 1. or 2.
     And to know which groups are "expanded" and which are not: boolean property per group
        
     EdgeGrouping could encapsulate the hierarchy and "expanded"
        
     class EdgeGrouping {
       List<EdgeGroup> getEdgeGroups()
       MinMax getWeightStats()
        
     }
        
     */

    //    return null;
    //  }

    /**
     * Creates a new FlowMapGraph in which nodes of this FlowMapGraph
     * having the same value of {@nodeAttrToGroupBy} are grouped together.
     */
    public FlowMapGraph groupNodesBy(String nodeAttrToGroupBy) {
        FlowMapGraphBuilder builder = new FlowMapGraphBuilder(getId(), attrSpec);
        //          .withCumulativeEdges()      // TODO: why isn't it working?

        Map<Object, Node> valueToNode = Maps.newHashMap();
        for (Object v : nodeAttrValues(nodeAttrToGroupBy)) {
            String strv = v.toString();
            Node node = builder.addNode(strv, new Point(0, 0), strv);
            valueToNode.put(v, node);
        }

        for (int i = 0, numEdges = graph.getEdgeCount(); i < numEdges; i++) {
            Edge e = graph.getEdge(i);
            Node src = e.getSourceNode();
            Node trg = e.getTargetNode();
            String srcV = src.getString(nodeAttrToGroupBy);
            String trgV = trg.getString(nodeAttrToGroupBy);
            if (srcV == null) {
                throw new IllegalArgumentException("No " + nodeAttrToGroupBy + " value for " + src);
            }
            if (trgV == null) {
                throw new IllegalArgumentException("No " + nodeAttrToGroupBy + " value for " + trg);
            }
            builder.addEdge(valueToNode.get(srcV), valueToNode.get(trgV), getEdgeWeights(e));
        }

        return builder.build();
    }

    //
    //  /**
    //   * Will add edges summarizing the weight attrs of the existing edges (which were not added by
    //   * another grouping) to the graph and mark them as grouping edges with the given
    //   * groupingName.
    //   */
    //  public void groupEdgesInPlace(String groupingName,
    //      Function<Edge, Object> groupFunction, GroupingEdgeFactory factory) {
    //    // Sort edges by groups
    //    Multimap<Object, Edge> mmap = ArrayListMultimap.create();
    //    for (Edge e : edges(null)) {
    //      mmap.put(groupFunction.apply(e), e);
    //    }
    //
    //    // Create grouped edges
    //    for (Object key : mmap.keySet()) {
    //      graph.addEdge();
    //      graph.getEdges().addTuple(factory.createGroupingEdge(mmap.get(key)));
    //    }
    //
    //  }
    //
    //  public interface GroupingEdgeFactory {
    //    Edge createGroupingEdge(Iterable<Edge> edges);
    //  }
    //
    //  public boolean isGroupingEdge(Edge e) {
    //    return (getEdgeGroupingName(e) != null);
    //  }
    //
    //  public String getEdgeGroupingName(Edge e) {
    //    if (!e.canGet(EDGE_GROUPING_COLUMN, String.class)) {
    //      return null;
    //    }
    //    return e.getString(EDGE_GROUPING_COLUMN);
    //  }
    //
    //  protected void setNameOfEdgeGrouping(Edge e, String nameOfTheGrouping) {
    //    if (e.canGet(EDGE_GROUPING_COLUMN, String.class)) {
    //      if (e.getString(EDGE_GROUPING_COLUMN) != null) {
    //        throw new IllegalArgumentException("Already a grouping edge");
    //      }
    //    } else {
    //      e.getTable().addColumn(EDGE_GROUPING_COLUMN, String.class);
    //    }
    //    e.setString(EDGE_GROUPING_COLUMN, nameOfTheGrouping);
    //  }

    public Map<String, String> mapOfNodeIdsToAttrValues(String nodeAttr) {
        Map<String, String> nodeIdsToLabels = Maps.newLinkedHashMap();
        for (int i = 0, numNodes = graph.getNodeCount(); i < numNodes; i++) {
            Node node = graph.getNode(i);
            nodeIdsToLabels.put(getNodeId(node), node.getString(nodeAttr));
        }
        return nodeIdsToLabels;
    }

    /**
     * Builds a multimap between the values of the given {@nodeAttr} and the
     * node ids having these values.
     */
    public Multimap<Object, String> multimapOfNodeAttrValuesToNodeIds(String nodeAttr) {
        Multimap<Object, String> multimap = LinkedHashMultimap.create();
        for (int i = 0, numNodes = graph.getNodeCount(); i < numNodes; i++) {
            Node node = graph.getNode(i);
            multimap.put(node.get(nodeAttr), getNodeId(node));
        }
        return multimap;
    }

    public static Function<Graph, FlowMapGraph> funcGraphToFlowMapGraph(final FlowMapAttrSpec attrSpec,
            final FlowMapStats stats) {
        return new Function<Graph, FlowMapGraph>() {
            @Override
            public FlowMapGraph apply(Graph from) {
                return new FlowMapGraph(from, attrSpec, stats);
            }
        };
    }

    /**
     * Loads a flow map graph from the given file and with the given attrSpecs.
     * If there are more than one graphs in the file only the first one will be used.
     */
    public static FlowMapGraph loadGraphML(String filename, FlowMapAttrSpec attrSpec) throws IOException {
        return loadGraphML(filename, attrSpec, null);
    }

    public static FlowMapGraph loadGraphML(GraphMLDatasetSpec dataset) throws IOException {
        return loadGraphML(dataset, null);
    }

    public static FlowMapGraph loadGraphML(GraphMLDatasetSpec dataset, FlowMapStats stats) throws IOException {
        Iterator<Graph> it = StaxGraphMLReader.readGraphs(dataset.getFilename()).iterator();
        if (!it.hasNext()) {
            throw new IOException("No graphs found in '" + dataset.getFilename() + "'");
        }
        Graph graph = it.next();
        return new FlowMapGraph(graph, dataset.createFlowMapAttrsSpecFor(graph), stats);
    }

    /**
     * Use when the stats have to be induced and not calculated (e.g. when a global mapping over
     * a number of flow maps for small multiples must be used).
     * Otherwise, use {@link #loadGraphML(String, FlowMapAttrSpec)}.
     */
    public static FlowMapGraph loadGraphML(String filename, FlowMapAttrSpec attrSpec, FlowMapStats stats)
            throws IOException {
        List<FlowMapGraph> list = FlowMapGraphSet.loadGraphMLAsList(filename, attrSpec, stats);
        if (list.isEmpty()) {
            throw new IOException("No graphs found in '" + filename + "'");
        }
        return list.get(0);
    }

    /**
     * Returns max node/edge weight over all specified attrs
     */
    public double getMaxAttrValue(Tuple nodeOrEdge, Iterable<String> attrNames) {
        double max = Double.NaN;
        for (String attr : attrNames) {
            if (!nodeOrEdge.canGetDouble(attr)) {
                throw new IllegalArgumentException("Cannot get double value of "
                        + nodeOrEdge.getClass().getSimpleName() + "'s attribute '" + attr + "'");
            }
            double v = nodeOrEdge.getDouble(attr);
            if (Double.isNaN(max) || v > max) {
                max = v;
            }
        }
        return max;
    }

    public double getAvgAttrValue(Tuple nodeOrEdge, Iterable<String> attrNames) {
        double sum = 0;
        int cnt = 0;
        for (String attr : attrNames) {
            double v = nodeOrEdge.getDouble(attr);
            if (!Double.isNaN(v)) {
                sum += v;
                cnt++;
            }
        }
        return (cnt == 0 ? Double.NaN : sum / cnt);
    }

    public Comparator<Node> createMaxNodeAttrValueComparator(final Iterable<String> nodeAttrs) {
        return new Comparator<Node>() {
            @Override
            public int compare(Node n1, Node n2) {
                return MathUtils.compareDoubles_smallestIsNaN(getMaxAttrValue(n1, nodeAttrs),
                        getMaxAttrValue(n2, nodeAttrs));
            }
        };
    }

    public Comparator<Edge> createMaxEdgeWeightComparator() {
        return createMaxEdgeWeightComparator(getEdgeWeightAttrs());
    }

    public Comparator<Edge> createMaxEdgeWeightDiffComparator() {
        return createMaxEdgeWeightComparator(getEdgeWeightDiffAttr());
    }

    public Comparator<Edge> createMaxEdgeWeightRelativeDiffComparator() {
        return createMaxEdgeWeightComparator(getEdgeWeightRelativeDiffAttrNames());
    }

    public Comparator<Edge> createMaxEdgeWeightComparator(final String weightAttr) {
        return new Comparator<Edge>() {
            @Override
            public int compare(Edge e1, Edge e2) {
                return MathUtils.compareDoubles_smallestIsNaN(e1.getDouble(weightAttr), e2.getDouble(weightAttr));
            }
        };
    }

    public Comparator<Edge> createMaxEdgeWeightComparator(final List<String> weightAttrs) {
        return new Comparator<Edge>() {
            @Override
            public int compare(Edge e1, Edge e2) {
                return MathUtils.compareDoubles_smallestIsNaN(getMaxAttrValue(e1, weightAttrs),
                        getMaxAttrValue(e2, weightAttrs));
            }
        };
    }

    public Comparator<Edge> createMaxNodeSummariesForWeightComparator(FlowEndpoint s) {
        return createMaxNodeSummariesComparator(getEdgeWeightAttrs(), s);
    }

    private Comparator<Edge> createMaxNodeSummariesComparator(final List<String> attrs, final FlowEndpoint s) {
        return new Comparator<Edge>() {
            @Override
            public int compare(Edge e1, Edge e2) {
                int c = MathUtils.compareDoubles_smallestIsNaN(
                        getMaxAttrValue(s.nodeOf(e1), FlowMapNodeTotals.getWeightTotalsNodeAttrs(attrs, s.dir())),
                        getMaxAttrValue(s.nodeOf(e2), FlowMapNodeTotals.getWeightTotalsNodeAttrs(attrs, s.dir())));

                if (c == 0) {
                    c = getNodeLabel(s.nodeOf(e1)).compareTo(getNodeLabel(s.nodeOf(e2)));
                }
                if (c == 0) {
                    c = MathUtils.compareDoubles_smallestIsNaN(getMaxAttrValue(e1, attrs),
                            getMaxAttrValue(e2, attrs));
                }
                return c;
            }
        };
    }

    public Comparator<Edge> createAvgEdgeWeightComparator() {
        return createAvgEdgeWeightComparator(getEdgeWeightAttrs());
    }

    public Comparator<Edge> createAvgEdgeWeightDiffComparator() {
        return createAvgEdgeWeightComparator(getEdgeWeightDiffAttr());
    }

    public Comparator<Edge> createAvgEdgeWeightComparator(final Iterable<String> attrNames) {
        return new Comparator<Edge>() {
            @Override
            public int compare(Edge e1, Edge e2) {
                return MathUtils.compareDoubles_smallestIsNaN(getAvgAttrValue(e1, attrNames),
                        getAvgAttrValue(e2, attrNames));
            }
        };
    }

    public void addEdgeWeightDifferenceColumns() {
        Iterable<Edge> edges = edges();

        String prevAttr = null;
        for (String attr : getEdgeWeightAttrs()) {
            String diffAttr = getAttrSpec().getFlowWeightDiffAttr(attr);

            graph.getEdges().addColumn(diffAttr, double.class);

            for (Edge edge : edges) {
                double prevVal = Double.NaN;
                if (prevAttr != null) {
                    prevVal = edge.getDouble(prevAttr);
                }
                // if there is no prev value, we'll assume it's zero,
                // so that we can at least see the absolute value
                //        double diff = edge.getDouble(attr) - (Double.isNaN(prevVal) ? 0 : prevVal);

                double diff = edge.getDouble(attr) - prevVal;
                edge.setDouble(diffAttr, diff);
            }

            prevAttr = attr;
        }
    }

    public void addEdgeWeightRelativeDifferenceColumns() {
        Iterable<Edge> edges = edges();

        String prevAttr = null;
        for (String attr : getEdgeWeightAttrs()) {
            String diffAttr = getAttrSpec().getFlowWeightRelativeDiffAttr(attr);

            graph.getEdges().addColumn(diffAttr, double.class);

            for (Edge edge : edges) {
                double rdiff = Double.NaN;
                double prevVal = Double.NaN;
                if (prevAttr != null) {
                    prevVal = edge.getDouble(prevAttr);
                    if (!Double.isNaN(prevVal)) {
                        double val = edge.getDouble(attr);
                        if (prevVal == 0) {
                            if (val == 0) {
                                rdiff = 0;
                            } else {
                                rdiff = Math.signum(val);
                            }
                        } else {
                            rdiff = (val - prevVal) / prevVal;
                            //rdiff = Math.abs(val - prevVal) / ((Math.abs(val) + Math.abs(prevVal))/2);
                        }
                        //            rdiff = MathUtils.relativeDiff(val, prevVal);
                    }
                }
                edge.setDouble(diffAttr, rdiff);
            }

            prevAttr = attr;
        }
    }

    public String getSourceNodeId(Edge edge) {
        return getNodeId(edge.getSourceNode());
    }

    public String getTargetNodeId(Edge edge) {
        return getNodeId(edge.getTargetNode());
    }

    public static final Comparator<? super FlowMapGraph> COMPARE_BY_GRAPH_IDS = new Comparator<FlowMapGraph>() {
        @Override
        public int compare(FlowMapGraph o1, FlowMapGraph o2) {
            return o1.getId().compareTo(o2.getId());
        }
    };

    public static final Comparator<Node> COMPARE_NODES_BY_IDS = new Comparator<Node>() {
        @Override
        public int compare(Node o1, Node o2) {
            return getIdOfNode(o1).compareTo(getIdOfNode(o2));
        }
    };

    private List<String> aggregatableNodeColumns;

    public List<String> getAggregatableNodeColumns() {
        if (aggregatableNodeColumns == null) {
            List<String> columns = Lists.newArrayList();
            columns.add(GRAPH_NODE_ID_COLUMN);
            columns.add(attrSpec.getNodeLabelAttr());
            if (attrSpec.hasNodePositions()) {
                columns.add(attrSpec.getNodeLonAttr());
                columns.add(attrSpec.getNodeLatAttr());
            }
            aggregatableNodeColumns = ImmutableList.copyOf(columns);
        }
        return aggregatableNodeColumns;
    }

    public List<String> getAggregatableEdgeColumns() {
        return getEdgeWeightAttrs();
    }

    public boolean hasNonZeroWeight(Edge edge) {
        for (String attr : getEdgeWeightAttrs()) {
            if (!Double.isNaN(getEdgeWeight(edge, attr))) {
                return true;
            }
        }

        return false;
    }

    public Iterable<Node> sortByAttr(Iterable<Node> nodes, final String attr) {
        List<Node> list = Lists.newArrayList(nodes);
        final AttrDataTypes type = AttrDataTypes.getByType(graph.getNodeTable().getColumnType(attr));
        Collections.sort(list, new Comparator<Node>() {
            @Override
            public int compare(Node n1, Node n2) {
                return type.compare(n1.get(attr), n2.get(attr));
            }
        });

        return list;
    }

    public List<String> getEdgeWeightDiffAttr() {
        return getAttrSpec().getFlowWeightDiffAttrs();
    }

    public List<String> getEdgeWeightRelativeDiffAttrNames() {
        return getAttrSpec().getFlowWeightRelativeDiffAttrs();
    }

    private Edge eddeForSimilaritySorting;

    public Edge getEgdeForSimilaritySorting() {
        if (eddeForSimilaritySorting == null) {
            List<Edge> edges = Lists.newArrayList(edges());
            Collections.sort(edges, createAvgEdgeWeightComparator()); //createAvgEdgeWeightDiffComparator());
            return edges.get(0);
        }
        return eddeForSimilaritySorting;
    }

    public void setEgdeForSimilaritySorting(Edge edge) {
        eddeForSimilaritySorting = edge;
    }

    public static Iterable<String> nodeAttrsOf(final Graph graph, final Class<?> ofType) {
        return attrsOf(graph.getNodeTable(), new Predicate<String>() {
            @Override
            public boolean apply(String attr) {
                return FlowMapGraph.isNodeSelfAttr(attr)
                        && (ofType == null || graph.getNodeTable().getColumnType(attr) == ofType);
            }
        });
    }

    public static Iterable<String> edgeAttrsOf(final Graph graph, final Class<?> ofType) {
        return attrsOf(graph.getEdgeTable(), new Predicate<String>() {
            @Override
            public boolean apply(String attr) {
                return FlowMapGraph.isEdgeSelfAttr(attr)
                        && (ofType == null || graph.getNodeTable().getColumnType(attr) == ofType);
            }
        });
    }

    private static Iterable<String> attrsOf(Table table, Predicate<String> isValid) {
        int cnt = table.getColumnCount();
        List<String> nodeAttrs = new ArrayList<String>(cnt);
        for (int i = 0; i < cnt; i++) {
            String attr = table.getColumnName(i);
            if (isValid.apply(attr)) {
                nodeAttrs.add(attr);
            }
        }
        return nodeAttrs;
    }

    public Iterable<String> nodeIdsOf(Iterable<Node> nodes) {
        return Iterables.transform(nodes, new Function<Node, String>() {
            @Override
            public String apply(Node node) {
                return FlowMapGraph.getIdOfNode(node);
            }
        });
    }

    public static boolean isNodeSelfAttr(String attrName) {
        return !attrName.equals(GRAPH_NODE_ID_COLUMN);
    }

    public static boolean isEdgeSelfAttr(String attrName) {
        return !attrName.equals(SRC_TEMP_ID) && !attrName.equals(TRG_TEMP_ID);
    }

}