blob: 0b4d130814fd434fd07aa0db2015a7304f819977 [file] [log] [blame]
/*
* Copyright 2015-present Open Networking Laboratory
*
* 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 org.onlab.stc;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* Computes scenario process flow layout for the Monitor GUI.
*/
public class MonitorLayout {
public static final int WIDTH = 210;
public static final int HEIGHT = 30;
public static final int W_GAP = 40;
public static final int H_GAP = 50;
public static final int SLOT_WIDTH = WIDTH + H_GAP;
private final Compiler compiler;
private final ProcessFlow flow;
private Map<Step, Box> boxes = Maps.newHashMap();
/**
* Creates a new shared process flow monitor.
*
* @param compiler scenario compiler
*/
MonitorLayout(Compiler compiler) {
this.compiler = compiler;
this.flow = compiler.processFlow();
// Extract the flow and create initial bounding boxes.
boxes.put(null, new Box(null, 0));
flow.getVertexes().forEach(this::createBox);
computeLayout(null, 0, 1);
}
// Computes the graph layout giving preference to group associations.
private void computeLayout(Group group, int absoluteTier, int tier) {
Box box = boxes.get(group);
// Find all children of the group, or items with no group if at top.
Set<Step> children = group != null ? group.children() :
flow.getVertexes().stream().filter(s -> s.group() == null)
.collect(Collectors.toSet());
children.forEach(s -> visit(s, absoluteTier, 1, group));
// Figure out what the group root vertexes are.
Set<Step> roots = findRoots(group);
// Compute the boxes for each of the roots.
roots.forEach(s -> updateBox(s, absoluteTier + 1, 1, group));
// Update the tier and depth of the group bounding box.
computeTiersAndDepth(group, box, absoluteTier, tier, children);
// Compute the minimum breadth of this group's bounding box.
computeBreadth(group, box, children);
// Compute child placements
computeChildPlacements(group, box, children);
}
// Updates the box for the specified step, given the tier number, which
// is relative to the parent.
private Box updateBox(Step step, int absoluteTier, int tier, Group group) {
Box box = boxes.get(step);
if (step instanceof Group) {
computeLayout((Group) step, absoluteTier, tier);
} else {
box.setTierAndDepth(absoluteTier, tier, 1, group);
}
// Follow the steps downstream of this one.
follow(step, absoluteTier + box.depth(), box.tier() + box.depth());
return box;
}
// Backwards follows edges leading towards the specified step to visit
// the source vertex and compute layout of those vertices that had
// sufficient number of visits to compute their tier.
private void follow(Step step, int absoluteTier, int tier) {
Group from = step.group();
flow.getEdgesTo(step).stream()
.filter(d -> visit(d.src(), absoluteTier, tier, from))
.forEach(d -> updateBox(d.src(), absoluteTier, tier, from));
}
// Visits each step, records maximum tier and returns true if this
// was the last expected visit.
private boolean visit(Step step, int absoluteTier, int tier, Group from) {
Box box = boxes.get(step);
return box.visitAndLatchMaxTier(absoluteTier, tier, from);
}
// Computes the absolute and relative tiers and the depth of the group
// bounding box.
private void computeTiersAndDepth(Group group, Box box,
int absoluteTier, int tier, Set<Step> children) {
int depth = children.stream().mapToInt(this::bottomMostTier).max().getAsInt();
box.setTierAndDepth(absoluteTier, tier, depth, group);
}
// Returns the bottom-most tier this step occupies relative to its parent.
private int bottomMostTier(Step step) {
Box box = boxes.get(step);
return box.tier() + box.depth();
}
// Computes breadth of the specified group.
private void computeBreadth(Group group, Box box, Set<Step> children) {
if (box.breadth() == 0) {
// Scan through all tiers and determine the maximum breadth of each.
IntStream.range(1, box.depth)
.forEach(t -> computeTierBreadth(t, box, children));
box.latchBreadth(children.stream()
.mapToInt(s -> boxes.get(s).breadth())
.max().getAsInt());
}
}
// Computes tier width.
private void computeTierBreadth(int t, Box box, Set<Step> children) {
box.latchBreadth(children.stream().map(boxes::get)
.filter(b -> isSpanningTier(b, t))
.mapToInt(Box::breadth).sum());
}
// Computes the actual child box placements relative to the parent using
// the previously established tier, depth and breadth attributes.
private void computeChildPlacements(Group group, Box box,
Set<Step> children) {
// Order the root-nodes in alphanumeric order first.
List<Box> tierBoxes = Lists.newArrayList(boxesOnTier(1, children));
tierBoxes.sort((a, b) -> a.step().name().compareTo(b.step().name()));
// Place the boxes centered on the parent box; left to right.
int tierBreadth = tierBoxes.stream().mapToInt(Box::breadth).sum();
int slot = 1;
for (Box b : tierBoxes) {
b.updateCenter(1, slot(slot, tierBreadth));
slot += b.breadth();
}
}
// Returns the horizontal offset off the parent center.
private int slot(int slot, int tierBreadth) {
boolean even = tierBreadth % 2 == 0;
int multiplier = -tierBreadth / 2 + slot - 1;
return even ? multiplier * SLOT_WIDTH + SLOT_WIDTH / 2 : multiplier * SLOT_WIDTH;
}
// Returns a list of all child step boxes that start on the specified tier.
private List<Box> boxesOnTier(int tier, Set<Step> children) {
return boxes.values().stream()
.filter(b -> b.tier() == tier && children.contains(b.step()))
.collect(Collectors.toList());
}
// Determines whether the specified box spans, or occupies a tier.
private boolean isSpanningTier(Box b, int tier) {
return (b.depth() == 1 && b.tier() == tier) ||
(b.tier() <= tier && tier < b.tier() + b.depth());
}
// Determines roots of the specified group or of the entire graph.
private Set<Step> findRoots(Group group) {
Set<Step> steps = group != null ? group.children() : flow.getVertexes();
return steps.stream().filter(s -> isRoot(s, group)).collect(Collectors.toSet());
}
private boolean isRoot(Step step, Group group) {
if (step.group() != group) {
return false;
}
Set<Dependency> requirements = flow.getEdgesFrom(step);
return requirements.stream().filter(r -> r.dst().group() == group)
.collect(Collectors.toSet()).isEmpty();
}
/**
* Returns the bounding box for the specified step. If null is given, it
* returns the overall bounding box.
*
* @param step step or group; null for the overall bounding box
* @return bounding box
*/
public Box get(Step step) {
return boxes.get(step);
}
/**
* Returns the bounding box for the specified step name. If null is given,
* it returns the overall bounding box.
*
* @param name name of step or group; null for the overall bounding box
* @return bounding box
*/
public Box get(String name) {
return get(name == null ? null : compiler.getStep(name));
}
// Creates a bounding box for the specified step or group.
private void createBox(Step step) {
boxes.put(step, new Box(step, flow.getEdgesFrom(step).size()));
}
/**
* Bounding box data for a step or group.
*/
final class Box {
private Step step;
private int remainingRequirements;
private int absoluteTier = 0;
private int tier;
private int depth = 1;
private int breadth;
private int center, top;
private Box(Step step, int remainingRequirements) {
this.step = step;
this.remainingRequirements = remainingRequirements + 1;
breadth = step == null || step instanceof Group ? 0 : 1;
}
private void latchTiers(int absoluteTier, int tier, Group from) {
this.absoluteTier = Math.max(this.absoluteTier, absoluteTier);
if (step == null || step.group() == from) {
this.tier = Math.max(this.tier, tier);
}
}
public void latchBreadth(int breadth) {
this.breadth = Math.max(this.breadth, breadth);
}
void setTierAndDepth(int absoluteTier, int tier, int depth, Group from) {
latchTiers(absoluteTier, tier, from);
this.depth = depth;
}
boolean visitAndLatchMaxTier(int absoluteTier, int tier, Group from) {
latchTiers(absoluteTier, tier, from);
--remainingRequirements;
return remainingRequirements == 0;
}
Step step() {
return step;
}
public int absoluteTier() {
return absoluteTier;
}
int tier() {
return tier;
}
int depth() {
return depth;
}
int breadth() {
return breadth;
}
int top() {
return top;
}
int center() {
return center;
}
public void updateCenter(int top, int center) {
this.top = top;
this.center = center;
}
}
}