/*
* Copyright 2017 ThoughtWorks, Inc.
*
* 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 com.thoughtworks.go.server.valuestreammap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.thoughtworks.go.domain.valuestreammap.Node;
import com.thoughtworks.go.domain.valuestreammap.NodeLevelMap;
import static java.lang.Math.abs;
import static java.util.Collections.sort;
public class CrossingMinimization {
private static final int LEVEL_OF_CURRENT_PIPELINE = 0;
public void apply(NodeLevelMap levelToNodesMap) {
initializeNodeDepths(levelToNodesMap);
reorderByBaryCenter(new LeftToRight(levelToNodesMap));
reorderByBaryCenter(new RightToLeft(levelToNodesMap));
normalizeUpstream(levelToNodesMap);
normalizeDownstream(levelToNodesMap);
}
private void normalizeUpstream(NodeLevelMap nodeLevelMap) {
reorderByMinDepth(new RightToLeft(nodeLevelMap, LEVEL_OF_CURRENT_PIPELINE - 2));
}
private void normalizeDownstream(NodeLevelMap nodeLevelMap) {
reorderByMinDepth(new LeftToRight(nodeLevelMap, LEVEL_OF_CURRENT_PIPELINE + 2));
}
private void reorderByMinDepth(TraversalDirection traversalDirection) {
while (traversalDirection.hasNext()) {
List<Node> nodesAtLevel = traversalDirection.next();
int depth = 1;
for (int i = 0; i < nodesAtLevel.size(); i++) {
Node currentNode = nodesAtLevel.get(i);
List<Node> relatedNodesAtPreviousLevel = traversalDirection.getRelatedNodesAtPreviousLevel(currentNode);
int leastDepth = minDepth(relatedNodesAtPreviousLevel);
if (depth < leastDepth) {
List<Node> nodesRemaining = nodesAtLevel.subList(i, nodesAtLevel.size());
int initialSlope = calculateSlope(depth - currentNode.getDepth(), nodesRemaining, traversalDirection);
int newSlope = calculateSlope(leastDepth - depth, nodesRemaining, traversalDirection);
if (newSlope < initialSlope) {
depth = leastDepth;
}
}
currentNode.setDepth(depth++);
}
}
}
private int calculateSlope(int depthOffset, List<Node> nodes, TraversalDirection traversalDirection) {
int totalSlope = 0;
for (Node node : nodes) {
for (Node relatedNode : traversalDirection.getRelatedNodesAtPreviousLevel(node)) {
totalSlope += abs(node.getDepth() + depthOffset - relatedNode.getDepth());
}
}
return totalSlope;
}
private int minDepth(List<Node> nodes) {
int min = Integer.MAX_VALUE;
for (Node node : nodes) {
int depth = node.getDepth();
if (min > depth) {
min = depth;
}
}
return min;
}
private void reorderByBaryCenter(TraversalDirection traversalDirection) {
while (traversalDirection.hasNext()) {
List<Node> nodesAtLevel = traversalDirection.next();
ArrayList<NodeBaryCentre> nodeBaryCentres = new ArrayList<>();
for (Node node : nodesAtLevel) {
List<Node> relatedNodes = traversalDirection.getRelatedNodesAtPreviousLevel(node);
nodeBaryCentres.add(getNodeBaryCentre(node, relatedNodes));
}
sort(nodeBaryCentres);
updateNodeDepths(nodeBaryCentres);
sort(nodesAtLevel);
}
}
private NodeBaryCentre getNodeBaryCentre(Node node, List<Node> relatedNodes) {
if (relatedNodes.isEmpty()) {
return new NodeBaryCentre(node, Float.valueOf(node.getDepth()));
}
float sum = 0f;
for (Node relatedNode : relatedNodes) {
int depth = relatedNode.getDepth();
sum += depth;
}
float averageDepth = sum / relatedNodes.size();
return new NodeBaryCentre(node, averageDepth);
}
private void updateNodeDepths(ArrayList<NodeBaryCentre> nodeBaryCentres) {
int depth = 1;
for (NodeBaryCentre nodeBaryCentre : nodeBaryCentres) {
nodeBaryCentre.node.setDepth(depth++);
}
}
void initializeNodeDepths(NodeLevelMap nodeLevelMap) {
Node pipeline = nodeLevelMap.get(LEVEL_OF_CURRENT_PIPELINE).get(0);
HashMap<Integer, Integer> levelToDepthMap = new HashMap<>();
initializeDepthsFor(pipeline, levelToDepthMap, new RightToLeft(nodeLevelMap, LEVEL_OF_CURRENT_PIPELINE), new HashSet<>());
initializeDepthsFor(pipeline, levelToDepthMap, new LeftToRight(nodeLevelMap, LEVEL_OF_CURRENT_PIPELINE), new HashSet<>());
}
private void initializeDepthsFor(Node node, HashMap<Integer, Integer> levelToDepthMap, TraversalDirection traversalDirection, Set<Node> visited) {
if (visited.contains(node)) {
return;
}
visited.add(node);
int depth = 1;
if (levelToDepthMap.containsKey(node.getLevel())) {
depth = levelToDepthMap.get(node.getLevel()) + 1;
}
if (node.getDepth() == 0) {
node.setDepth(depth);
levelToDepthMap.put(node.getLevel(), depth);
}
for (Node relatedNode : traversalDirection.getRelatedNodesAtNextLevel(node)) {
initializeDepthsFor(relatedNode, levelToDepthMap, traversalDirection, visited);
}
}
private interface TraversalDirection {
boolean hasNext();
List<Node> next();
List<Node> getRelatedNodesAtPreviousLevel(Node node);
List<Node> getRelatedNodesAtNextLevel(Node node);
}
private class LeftToRight implements TraversalDirection {
private final NodeLevelMap nodeLevelMap;
private int index;
public LeftToRight(NodeLevelMap nodeLevelMap) {
this(nodeLevelMap, nodeLevelMap.lowestLevel());
}
public LeftToRight(NodeLevelMap nodeLevelMap, int startIndex) {
this.nodeLevelMap = nodeLevelMap;
this.index = startIndex;
}
@Override
public boolean hasNext() {
return nodeLevelMap.get(index) != null;
}
@Override
public List<Node> next() {
return nodeLevelMap.get(index++);
}
@Override
public List<Node> getRelatedNodesAtPreviousLevel(Node node) {
return node.getParents();
}
@Override
public List<Node> getRelatedNodesAtNextLevel(Node node) {
return node.getChildren();
}
}
private class RightToLeft implements TraversalDirection {
private final NodeLevelMap nodeLevelMap;
private int index;
public RightToLeft(NodeLevelMap nodeLevelMap) {
this(nodeLevelMap, nodeLevelMap.highestLevel());
}
public RightToLeft(NodeLevelMap nodeLevelMap, int startIndex) {
this.nodeLevelMap = nodeLevelMap;
this.index = startIndex;
}
@Override
public boolean hasNext() {
return nodeLevelMap.get(index) != null;
}
@Override
public List<Node> next() {
return nodeLevelMap.get(index--);
}
@Override
public List<Node> getRelatedNodesAtPreviousLevel(Node node) {
return node.getChildren();
}
@Override
public List<Node> getRelatedNodesAtNextLevel(Node node) {
return node.getParents();
}
}
private class NodeBaryCentre implements Comparable {
private final Node node;
private final Float averageDepth;
public NodeBaryCentre(Node node, Float averageDepth) {
this.node = node;
this.averageDepth = averageDepth;
}
@Override
public int compareTo(Object other) {
return this.averageDepth.compareTo(((NodeBaryCentre) other).averageDepth);
}
}
}