jflowmap.views.flowstrates.MapLayer.java Source code

Java tutorial

Introduction

Here is the source code for jflowmap.views.flowstrates.MapLayer.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.views.flowstrates;

import static jflowmap.data.FlowMapGraphEdgeAggregator.getAggregateList;
import static jflowmap.data.FlowMapGraphEdgeAggregator.isAggregate;

import java.awt.Color;
import java.awt.Shape;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jflowmap.FlowEndpoint;
import jflowmap.FlowMapGraph;
import jflowmap.data.FlowMapGraphEdgeAggregator;
import jflowmap.data.FlowMapNodeTotals;
import jflowmap.data.Nodes;
import jflowmap.data.SeqStat;
import jflowmap.geo.MapProjections;
import jflowmap.geom.GeomUtils;
import jflowmap.models.map.GeoMap;
import jflowmap.models.map.MapArea;
import jflowmap.models.map.Polygon;
import jflowmap.util.CollectionUtils;
import jflowmap.util.piccolo.PNodes;
import jflowmap.util.piccolo.PTypedBasicInputEventHandler;
import jflowmap.views.ColorCodes;
import jflowmap.views.flowmap.ColorSchemeAware;
import jflowmap.views.map.PGeoMap;
import jflowmap.views.map.PGeoMapArea;
import prefuse.data.Edge;
import prefuse.data.Node;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import edu.umd.cs.piccolo.PCamera;
import edu.umd.cs.piccolo.PLayer;
import edu.umd.cs.piccolo.event.PInputEvent;
import edu.umd.cs.piccolo.event.PInputEventListener;
import edu.umd.cs.piccolo.util.PBounds;

/**
 * @author Ilya Boyandin
 */
public class MapLayer extends PLayer implements ViewLayer {

    private static final double CENTROID_DOT_SIZE = 2.0;

    private final PCamera geoLayerCamera;
    private final PGeoMap visualAreaMap;
    private final FlowstratesView flowstratesView;
    private final FlowEndpoint endpoint;
    private Map<String, Centroid> nodeIdsToCentroids;
    private List<String> selectedNodes;

    private final Lasso lasso;
    private PInputEventListener centroidMouseListener;
    private boolean centroidsOpaque = true;
    private Rectangle2D centroidsBounds;

    public MapLayer(FlowstratesView flowstratesView, GeoMap areaMap, FlowEndpoint s) {
        this.flowstratesView = flowstratesView;
        this.endpoint = s;

        geoLayerCamera = new PCamera();
        geoLayerCamera.addLayer(this);

        visualAreaMap = new PGeoMap(flowstratesView.getMapColorScheme(), areaMap,
                flowstratesView.getMapProjection());

        addChild(visualAreaMap);
        geoLayerCamera.setPaint(visualAreaMap.getPaint());

        createCentroids();
        addMouseOverListenersToMaps(visualAreaMap); // must be after createCentroids

        visualAreaMap.setBounds(visualAreaMap.getFullBoundsReference()); // enable mouse ev.

        lasso = createLasso(geoLayerCamera);
        geoLayerCamera.addInputEventListener(lasso);

    }

    public FlowEndpoint getEndpoint() {
        return endpoint;
    }

    public PCamera getCamera() {
        return geoLayerCamera;
    }

    public PGeoMap getVisualAreaMap() {
        return visualAreaMap;
    }

    public Map<String, Centroid> getNodeIdsToCentroids() {
        return nodeIdsToCentroids;
    }

    private FlowMapGraph getFlowMapGraph() {
        return flowstratesView.getFlowMapGraph();
    }

    public void setCentroidsOpaque(boolean value) {
        if (centroidsOpaque != value) {
            centroidsOpaque = value;
            updateCentroids();
        }
    }

    void updateCentroids() {
        RectSet occupied = new RectSet(nodeIdsToCentroids.size());

        Set<String> left;

        if (isNodeSelectionEmpty()) {
            left = nodeIdsToCentroids.keySet();
        } else {
            // give priority to the selected nodes
            left = Sets.newLinkedHashSet(nodeIdsToCentroids.keySet());
            for (String id : selectedNodes) {
                updateCentroid(nodeIdsToCentroids.get(id), occupied);
                left.remove(id);
            }
        }

        for (String id : left) {
            updateCentroid(nodeIdsToCentroids.get(id), occupied);
        }
    }

    private void updateCentroid(Centroid c, RectSet occupied) {
        //    if (centroidsOpaque) {
        c.updateInCamera(geoLayerCamera);
        if (c.getVisible()) {
            c.getLabelNode().setVisible(occupied.addIfNotIntersects(c.getCollisionBounds()));
        }
        c.setOpaque(centroidsOpaque);
        //      c.setOpaque(true);
        //    } else {
        // boolean vis = false;
        // VisualArea va = getVisualAreaMap().getVisualAreaBy(c.getNodeId());
        // if (va != null) {
        // vis = va.getFullBoundsReference().contains(c.getLabelNode().getBoundsReference());
        // }
        // c.setVisible(vis);
        //      c.setOpaque(false);
        //    }
    }

    private void createCentroids() {
        nodeIdsToCentroids = Maps.newLinkedHashMap();

        FlowMapGraph flowMapGraph = flowstratesView.getFlowMapGraph();

        // sort centroids so that more important are shown first
        Iterable<Node> nodes = CollectionUtils.sort(flowMapGraph.nodesHavingEdges(endpoint.dir()), Collections
                .reverseOrder(FlowMapNodeTotals.createMaxNodeWeightTotalsComparator(flowMapGraph, endpoint.dir())));

        centroidMouseListener = createCentroidMouseListener();

        Iterable<Node> nodesWithCoords = Iterables.filter(nodes, haveCoordsPredicate());
        Iterable<Node> nodesWithoutCoords = flowMapGraph.sortByAttr(
                Iterables.filter(nodes, Predicates.not(haveCoordsPredicate())), flowMapGraph.getNodeLabelAttr());

        createCentroidsForNodesWithCoords(nodesWithCoords);
        createCentroidsAndAreasForNodesWithoutCoords(nodesWithoutCoords);
    }

    private void createCentroidsForNodesWithCoords(Iterable<Node> nodesWithCoords) {
        FlowMapGraph fmg = flowstratesView.getFlowMapGraph();

        for (Node node : nodesWithCoords) {
            Point2D p = visualAreaMap.getMapProjection().project(node.getDouble(fmg.getNodeLonAttr()),
                    node.getDouble(fmg.getNodeLatAttr()));

            createCentroid(node, p.getX(), p.getY());
        }
    }

    private void createCentroidsAndAreasForNodesWithoutCoords(Iterable<Node> nodesWithoutCoords) {
        Rectangle2D bounds = centroidsBounds();
        FlowMapGraph fmg = flowstratesView.getFlowMapGraph();

        int num = Iterables.size(nodesWithoutCoords);
        if (num > 0) {
            final int maxPerRow = 4;
            int cnt = 0;
            final int numPerRow = Math.min(maxPerRow, num);
            final int r = num % numPerRow;
            final double rwidth = bounds.getWidth() * 0.7 / (maxPerRow - 1);
            final double rheight = (bounds.getHeight() / 10);
            final double topMargin = bounds.getHeight() / 5;

            for (Node node : nodesWithoutCoords) {
                final int numInThisRow = (cnt >= (num - r) ? r : numPerRow);
                double hcentering = (bounds.getWidth() - (numInThisRow - 1) * rwidth) / 2;

                double x = hcentering + bounds.getMinX() + (cnt % numPerRow) * rwidth;
                double y = bounds.getMaxY() + topMargin + Math.floor(cnt / numPerRow) * rheight;
                createCentroid(node, x, y);

                String nodeId = fmg.getNodeId(node);
                String label = fmg.getNodeLabel(node);

                Rectangle2D rect = new Rectangle2D.Double(x - rwidth / 2, y - rheight / 3, rwidth, rheight);
                rect = GeomUtils.growRectByRelativeSize(rect, -0.05, -0.1, -0.05, -0.1);

                Polygon polygon = new Polygon(new Point2D[] { new Point2D.Double(rect.getX(), rect.getY()),
                        new Point2D.Double(rect.getMaxX(), rect.getY()),
                        new Point2D.Double(rect.getMaxX(), rect.getMaxY()),
                        new Point2D.Double(rect.getX(), rect.getMaxY()),
                        new Point2D.Double(rect.getX(), rect.getY()) });

                visualAreaMap.addArea(new MapArea(nodeId, label, Arrays.asList(polygon)), MapProjections.NONE);

                cnt++;
            }
        }
    }

    private void createCentroid(Node node, double x, double y) {
        FlowMapGraph flowMapGraph = flowstratesView.getFlowMapGraph();

        String nodeId = flowMapGraph.getNodeId(node);
        String nodeLabel = flowMapGraph.getNodeLabel(node);
        Centroid c = new Centroid(nodeId, nodeLabel, x, y, CENTROID_DOT_SIZE,
                flowstratesView.getStyle().getMapAreaCentroidPaint(), flowstratesView);
        // c.setPickable(false);

        c.addInputEventListener(centroidMouseListener);
        geoLayerCamera.addChild(c);
        nodeIdsToCentroids.put(c.getNodeId(), c);
    }

    private Predicate<Node> haveCoordsPredicate() {
        return new Predicate<Node>() {
            @Override
            public boolean apply(Node node) {
                FlowMapGraph fmg = flowstratesView.getFlowMapGraph();

                double lon = node.getDouble(fmg.getNodeLonAttr());
                double lat = node.getDouble(fmg.getNodeLatAttr());

                return !((Double.isNaN(lon) || lon == 0) && (Double.isNaN(lat) || lat == 0));
            }
        };
    }

    void setSelectedNodes(List<String> nodeIds) {
        if (selectedNodes != nodeIds) {
            flowstratesView.setCustomEdgeFilter(null);
            List<String> old = selectedNodes;
            selectedNodes = (nodeIds != null ? ImmutableList.copyOf(nodeIds) : null);
            flowstratesView.fireNodeSelectionChanged(old, nodeIds);
            flowstratesView.updateVisibleEdges();
            updateCentroidColors();
            flowstratesView.getTemporalLayer().fitInView(false, false);
        }
    }

    private Lasso createLasso(PCamera targetCamera) {
        return new Lasso(targetCamera, flowstratesView.getStyle().getLassoStrokePaint(endpoint)) {
            @Override
            public void selectionMade(Shape shape) {
                setSelectedNodes(applyLassoToNodeCentroids(shape));
            }
        };
    }

    /**
     * @returns List of ids of selected nodes, or null if no nodes were selected
     */
    private List<String> applyLassoToNodeCentroids(Shape shape) {
        List<String> nodeIds = null;
        for (Map.Entry<String, Centroid> e : nodeIdsToCentroids.entrySet()) {
            Centroid centroid = e.getValue();
            if (shape.contains(centroid.getPoint())) {
                if (nodeIds == null) {
                    nodeIds = Lists.newArrayList();
                }
                nodeIds.add(e.getKey());
            }
        }
        return nodeIds;
    }

    Point2D getCentroidPoint(Edge edge) {
        Centroid centroid = nodeIdsToCentroids.get(getFlowMapGraph().getNodeId(endpoint.nodeOf(edge)));
        Point2D point;
        if (centroid == null) {
            point = null;
        } else {
            point = centroid.getPoint();
        }
        return point;
    }

    public boolean isPointVisible(Point2D p) {
        return getCamera().getViewBounds().contains(p);
    }

    void updateCentroidColors() {
        for (Map.Entry<String, Centroid> e : nodeIdsToCentroids.entrySet()) {
            String nodeId = e.getKey();
            Centroid centroid = e.getValue();
            centroid.setSelected(nodeSelectionContains(nodeId));
        }
    }

    private PInputEventListener createCentroidMouseListener() {
        return new PTypedBasicInputEventHandler<Centroid>(Centroid.class) {
            @Override
            public void mouseEntered(PInputEvent event) {
                if (lasso.isSelecting())
                    return;

                Centroid c = node(event);
                if (c != null) {
                    setNodeHighlighted(c.getNodeId(), true);
                }
            }

            @Override
            public void mouseClicked(PInputEvent event) {
                if (lasso.isSelecting())
                    return;

                Centroid centroid = node(event);
                if (centroid != null) {
                    String nodeId = centroid.getNodeId();
                    onCentroidOrAreaMouseClicked(event, nodeId);
                }
            }

            @Override
            public void mouseExited(PInputEvent event) {
                Centroid c = node(event);
                if (c != null) {
                    setNodeHighlighted(c.getNodeId(), false);
                }
            }
        };
    }

    private void addMouseOverListenersToMaps(PGeoMap visualAreaMap) {
        PInputEventListener listener = new PTypedBasicInputEventHandler<PGeoMapArea>(PGeoMapArea.class) {
            @Override
            public void mouseEntered(PInputEvent event) {
                if (lasso.isSelecting())
                    return;

                if (event.isControlDown())
                    return;

                PGeoMapArea va = node(event);
                if (va != null) {
                    String areaId = va.getArea().getId();
                    if (nodeIdsToCentroids.containsKey(areaId)) {
                        setNodeHighlighted(areaId, true);
                    }
                }
            }

            @Override
            public void mouseClicked(PInputEvent event) {
                if (lasso.isSelecting())
                    return;

                PGeoMapArea va = node(event);
                if (va != null) {
                    String areaId = va.getArea().getId();
                    if (nodeIdsToCentroids.containsKey(areaId)) {
                        onCentroidOrAreaMouseClicked(event, areaId);
                    }
                }
            }

            @Override
            public void mouseExited(PInputEvent event) {
                PGeoMapArea va = node(event);
                if (va != null) {
                    String areaId = va.getArea().getId();
                    setNodeHighlighted(areaId, false);
                }
            }
        };
        for (PGeoMapArea va : PNodes.childrenOfType(visualAreaMap, PGeoMapArea.class)) {
            va.addInputEventListener(listener);
        }
    }

    private void onCentroidOrAreaMouseClicked(PInputEvent event, String nodeId) {
        List<String> newSelection;
        if (isNodeSelectionEmpty()) {
            newSelection = Arrays.asList(nodeId);
        } else {
            if (event.isControlDown()) {
                newSelection = Lists.newArrayList(selectedNodes);
                if (selectedNodes.contains(nodeId)) {
                    newSelection.remove(nodeId);
                } else {
                    newSelection.add(nodeId);
                }
            } else {
                if (selectedNodes.size() == 1 && selectedNodes.get(0).equals(nodeId)) {
                    // focusing on corresponding nodes in the opposite map when clicking on
                    // the only selected node
                    FlowEndpoint other = endpoint.opposite();
                    MapLayer otherMap = flowstratesView.getMapLayer(other);
                    //          focusOnNode(nodeId);
                    otherMap.focusOnNodesOfVisibleEdges();
                }
                newSelection = Arrays.asList(nodeId);
            }
        }
        setSelectedNodes(newSelection);
    }

    private void colorizeMapArea(String areaId, double value, boolean hover) {
        colorizeMapArea(areaId, value, hover, flowstratesView.getValueStat());
    }

    private void colorizeMapArea(String areaId, double value, boolean hover, SeqStat valueStat) {
        Centroid c = nodeIdsToCentroids.get(areaId);
        if (c != null) {
            PGeoMapArea area = visualAreaMap.getVisualAreaBy(areaId);
            if (area != null && !area.isEmpty()) {
                Color color;
                if (hover) {
                    color = flowstratesView.getColorFor(value, valueStat);
                } else {
                    color = flowstratesView.getMapColorScheme().getColor(ColorCodes.AREA_PAINT);
                }
                area.setPaint(color);
            } else {
                Color color;
                if (hover) {
                    color = flowstratesView.getColorFor(value, valueStat);
                } else {
                    color = flowstratesView.getStyle().getMapAreaCentroidLabelPaint();
                }
                c.getLabelNode().setPaint(color);
            }
        }
    }

    private void colorizeMapAreasWithNodeTotals(Iterable<Edge> edges, String weightAttr, boolean hover) {
        Map<String, Double> totals = calcNodeTotalsFor(edges, weightAttr);

        for (Node node : Nodes.distinctNodesOfEdges(edges, endpoint)) {
            String nodeId = getFlowMapGraph().getNodeId(node);
            colorizeMapArea(nodeId, totals.get(nodeId), hover);
        }
    }

    public Map<String, Double> calcNodeTotalsFor(Iterable<Edge> edges, String weightAttr) {
        return FlowMapNodeTotals.calcNodeTotalsFor(getFlowMapGraph(), edges, getColumnValueAttrName(weightAttr),
                endpoint);
    }

    void updateMapAreaColorsOnHeatmapCellHover(Edge edge, String weightAttr, boolean hover) {
        if (FlowMapGraphEdgeAggregator.isAggregate(edge)) {

            List<Edge> edges = FlowMapGraphEdgeAggregator.getBaseAggregateList(edge);
            colorizeMapAreasWithNodeTotals(edges, weightAttr, hover);

        } else {
            double value = flowstratesView.getValue(edge, weightAttr);
            colorizeMapArea(flowstratesView.getAggLayers().getNodeId(edge, endpoint), value, hover);
        }
    }

    void updateOnHeatmapColumnHover(String columnAttr, boolean hover) {
        Iterable<Edge> edges;
        // if (flowstratesView.isFilterApplied()) {
        edges = flowstratesView.getVisibleEdges();
        // } else {
        // edges = getFlowMapGraph().edges();
        // }
        setCentroidsOpaque(!hover);
        colorizeMapAreasWithNodeTotals(edges, columnAttr, hover);
    }

    private String getColumnValueAttrName(String columnAttr) {
        ValueType vtype = flowstratesView.getValueType();

        return vtype.getColumnValueAttr(getFlowMapGraph().getAttrSpec(), columnAttr);
    }

    void setVisualAreaMapHighlighted(String nodeId, boolean highlighted) {
        PGeoMapArea va = visualAreaMap.getVisualAreaBy(nodeId);
        FlowstratesStyle style = flowstratesView.getStyle();

        if (va != null) {
            va.moveToFront();
            if (highlighted) {
                va.setStroke(style.getMapAreaHighlightedStroke());
                va.setStrokePaint(style.getMapAreaHighlightedStrokePaint());
                va.setPaint(style.getMapAreaHighlightedPaint());
            } else {
                ColorSchemeAware cs = flowstratesView.getMapColorScheme();

                va.setStroke(style.getMapAreaStroke());
                va.setStrokePaint(cs.getColor(ColorCodes.AREA_STROKE));
                va.setPaint(cs.getColor(ColorCodes.AREA_PAINT));
            }
            va.repaint();
        }
    }

    Rectangle2D centroidsBounds() {
        return centroidsBounds(nodeIdsToCentroids.values());
    }

    Rectangle2D centroidsBounds(Iterable<Centroid> centroids) {
        if (centroidsBounds == null) {
            Rectangle2D.Double b = new Rectangle2D.Double();
            boolean first = true;
            for (Centroid c : centroids) {
                double x = c.getOrigX();
                double y = c.getOrigY();
                if (first) {
                    b.x = x;
                    b.y = y;
                    first = false;
                } else {
                    if (x < b.x) {
                        b.x = x;
                    }
                    if (x > b.getMaxX()) {
                        b.width = x - b.x;
                    }
                    if (y < b.y) {
                        b.y = y;
                    }
                    if (y > b.getMaxY()) {
                        b.height = y - b.y;
                    }
                }
            }
            // getVisualAreaMapCamera(s).localToView(b);
            centroidsBounds = b;
        }
        return new PBounds(centroidsBounds);
    }

    void updateOnHeatmapCellHover(Edge edge, String weightAttr, boolean hover) {
        updateMapAreaColorsOnHeatmapCellHover(edge, weightAttr, hover);
        setEdgeEndpointCentroidHighlighted(edge, hover);
        if (isAggregate(edge) && getAggregateList(edge).size() > 1) {
            setCentroidsOpaque(!hover);
        }
    }

    void setEdgeEndpointCentroidHighlighted(Edge edge, boolean highlighted) {
        Node node = getFlowMapGraph().getNodeOf(edge, endpoint);
        Centroid c = nodeIdsToCentroids.get(getFlowMapGraph().getNodeId(node));
        if (c != null) {
            c.setHighlighted(highlighted);
        }
    }

    public void setNodeHighlighted(final String nodeId, boolean highlighted) {
        if (nodeId == null) {
            return;
        }
        Centroid c = nodeIdsToCentroids.get(nodeId);
        if (c != null) {
            c.setHighlighted(highlighted);
            c.setTimeSliderVisible(highlighted);
        }
        Predicate<Node> acceptNodes = new Predicate<Node>() {
            @Override
            public boolean apply(Node node) {
                return nodeId.equals(getFlowMapGraph().getNodeId(node));
            }
        };

        FlowLinesLayerNode flowLinesLayer = flowstratesView.getFlowLinesLayerNode();

        flowLinesLayer.setFlowLinesColoringMode(FlowLinesColoringMode.of(endpoint));

        for (Edge e : endpoint.filterByNodePredicate(flowstratesView.getVisibleEdges(), acceptNodes)) {
            flowLinesLayer.setFlowLinesOfEdgeHighlighted(e, highlighted);
        }
        setVisualAreaMapHighlighted(nodeId, highlighted);
    }

    public boolean isNodeSelectionEmpty() {
        return selectedNodes == null || selectedNodes.isEmpty();
    }

    public boolean nodeSelectionContains(String nodeId) {
        if (selectedNodes == null) {
            return false;
        }
        return selectedNodes.contains(nodeId);
    }

    public boolean nodeSelectionContains(Node node) {
        return nodeSelectionContains(getFlowMapGraph().getNodeId(node));
    }

    public boolean clearNodeSelection() {
        if (selectedNodes == null) {
            return false;
        } else {
            selectedNodes = null;
            updateCentroidColors();
            return true;
        }
    }

    public void focusOnNode(String nodeId) {
        focusOnNodes(Arrays.asList(nodeId));
    }

    public void focusOnNodes(Iterable<String> nodeIds) {
        Rectangle2D.Double rect = getBoundingBoxFor(nodeIds);

        if (rect != null) {
            Rectangle2D fbb = getVisualAreaMap().getBoundingBox();

            final double fw = fbb.getWidth(), fh = fbb.getHeight();
            double w = rect.getWidth(), h = rect.getHeight();

            // Show more context for smaller areas and less for larger ones
            final double minGrowth = 1.1;
            final double maxGrowth = 3.4;
            double alpha = Math.min((fw - Math.min(fw, w * 5)) / fw, (fh - Math.min(fh, h * 5)) / fh);
            double growBy = minGrowth + (maxGrowth - minGrowth) * Math.pow(alpha, 5);
            GeomUtils.growRectInPlaceByRelativeSize(rect, growBy, growBy, growBy, growBy);

            //      if (rect.x < fbb.getX()) {
            //        rect.x = fbb.getX();
            //      }
            //      if (rect.y < fbb.getY()) {
            //        rect.y = fbb.getY();
            //      }

            if (rect.width > fbb.getWidth() /*||  rect.getMaxX() > fbb.getMaxX()*/) {
                rect.x = fbb.getX();
                rect.width = fbb.getWidth();
            }
            if (rect.height > fbb.getHeight() /*||  rect.getMaxX() > fbb.getMaxY()*/) {
                rect.y = fbb.getY();
                rect.height = fbb.getHeight();
            }

            getCamera().animateViewToCenterBounds(rect, true, FlowstratesView.fitInViewDuration(true));
        }
    }

    public void focusOnNodesOfVisibleEdges() {
        focusOnNodes(Nodes.nodeIdsOf(Nodes.distinctNodesOfEdges(flowstratesView.getVisibleEdges(), endpoint)));
    }

    private Rectangle2D.Double getBoundingBoxFor(Iterable<String> nodeIds) {
        Rectangle2D.Double rect = null;

        for (String nodeId : nodeIds) {
            Centroid c = nodeIdsToCentroids.get(nodeId);
            if (c != null) {
                if (rect == null) {
                    rect = new Rectangle2D.Double(c.getOrigX(), c.getOrigY(), 0, 0);
                } else {
                    rect.add(c.getOrigX(), c.getOrigY());
                }
            }

            PGeoMapArea va = visualAreaMap.getVisualAreaBy(nodeId);
            if (va != null) {
                if (rect == null) {
                    rect = new PBounds(va.getBoundingBox());
                } else {
                    rect.add(va.getBoundingBox());
                }
            }
        }
        return rect;
    }

    public void fitInView(boolean animate, boolean whole) {
        if (whole || isNodeSelectionEmpty()) {
            fit(centroidsBounds(), animate);
        } else {

            /*
            PBounds current = getCamera().getViewBounds();
            Rectangle2D full = centroidsBounds();
            //      Rectangle2D sel = getBoundingBoxFor(selectedNodes);
                
            double diff2full = Math.abs(MathUtils.relativeDiff(current.width, full.getWidth()));
            if (diff2full > 0.5) {
              fit(full, animate);
            } else {
            //        boundsToFit = getBoundingBoxFor(selectedNodes);
              focusOnNodes(selectedNodes);
            }
            */
            focusOnNodes(selectedNodes);

        }
    }

    private void fit(Rectangle2D boundsToFit, boolean animate) {
        GeomUtils.growRectInPlaceByRelativeSize(boundsToFit, 0, .1, 0, .1);
        getCamera().animateViewToCenterBounds(boundsToFit, true, FlowstratesView.fitInViewDuration(animate));
    }

}