package de.tud.inf.operator.mm;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import Jama.Matrix;
import com.rapidminer.example.Attribute;
import com.rapidminer.example.Example;
import com.rapidminer.example.ExampleSet;
import com.rapidminer.example.table.AttributeFactory;
import com.rapidminer.operator.IOObject;
import com.rapidminer.operator.Operator;
import com.rapidminer.operator.OperatorDescription;
import com.rapidminer.operator.OperatorException;
import com.rapidminer.parameter.ParameterType;
import com.rapidminer.parameter.ParameterTypeString;
import com.rapidminer.tools.Ontology;
import de.tud.inf.operator.mm.util.SortingIndex;
/**
* This class calculates the convex hull of a given input set.
*
* {@link http://www.ddj.com/architect/201806315}
*
* @version $Revision$
* @author Andre Jaehnig
*/
public class ConvexHullCalculator extends Operator {
/************************************************************************************************
* FIELDS
***********************************************************************************************/
/** Column name with the indicator of the selected items. */
public static final String CONVEX_HULL_MEMBER_COLUMN_NAME = "ch_member";
/************************************************************************************************
* GETTER & SETTER
***********************************************************************************************/
/*
* (non-Javadoc)
*
* @see com.rapidminer.operator.Operator#getInputClasses()
*/
@Override
public Class<?>[] getInputClasses() {
return new Class[] { ExampleSet.class };
}
/*
* (non-Javadoc)
*
* @see com.rapidminer.operator.Operator#getOutputClasses()
*/
@Override
public Class<?>[] getOutputClasses() {
return new Class[] { ExampleSet.class };
}
/*
* (non-Javadoc)
*
* @see com.rapidminer.operator.Operator#getParameterTypes()
*/
@Override
public List<ParameterType> getParameterTypes() {
List<ParameterType> types = super.getParameterTypes();
types.add(new ParameterTypeString(CONVEX_HULL_MEMBER_COLUMN_NAME,
"Column name with the indicator of the selected items.", "ch_member"));
return types;
}
/************************************************************************************************
* CONSTRUCTOR
***********************************************************************************************/
/**
* Constructor
*
* @param description
*/
public ConvexHullCalculator(OperatorDescription description) {
super(description);
}
/************************************************************************************************
* PUBLIC METHODS
***********************************************************************************************/
/*
* (non-Javadoc)
*
* @see com.rapidminer.operator.Operator#apply()
*/
@Override
public IOObject[] apply() throws OperatorException {
// get example set
ExampleSet exampleSet = this.getInput(ExampleSet.class);
int exampleSetSize = exampleSet.size();
this.logNote("Input example-set has " + exampleSetSize + " elements.");
// get parameters
String chColumnName = this.getParameterAsString(CONVEX_HULL_MEMBER_COLUMN_NAME);
// create attribute for the convex hull indicator
Attribute chMemberAttr = AttributeFactory.createAttribute(chColumnName, Ontology.NOMINAL);
exampleSet.getExampleTable().addAttribute(chMemberAttr);
// add attribute to view
exampleSet.getAttributes().setSpecialAttribute(chMemberAttr, CONVEX_HULL_MEMBER_COLUMN_NAME);
// get first attribute as sort attribute and all attributes as dimensions
Attribute sortAttr = null;
int dimAttributesCount = exampleSet.getAttributes().size();
Attribute[] dimAttributes = new Attribute[dimAttributesCount];
boolean first = true;
int counter = 0;
for (Attribute attr : exampleSet.getAttributes()) {
if (first) {
sortAttr = attr;
first = false;
}
dimAttributes[counter++] = attr;
}
// sort example set according to the sort attribute and set indicator to false
List<SortingIndex> sortingIndex = new ArrayList<SortingIndex>(exampleSetSize);
counter = 0;
Iterator<Example> it = exampleSet.iterator();
while (it.hasNext()) {
Example example = it.next();
sortingIndex.add(new SortingIndex(Double.valueOf(example.getNumericalValue(sortAttr)), counter));
counter++;
// indicator to default value false
example.setValue(chMemberAttr, "false");
}
Collections.sort(sortingIndex);
/*
* put all data into a stack
*
* one stack entry is a vector with the id of the example set and the dimension-attribute
* values
*/
Stack<List<Double>> points = new Stack<List<Double>>();
Iterator<SortingIndex> k = sortingIndex.iterator();
while (k.hasNext()) {
int index = k.next().getIndex();
Example example = exampleSet.getExample(index);
List<Double> entry = new ArrayList<Double>();
// id
entry.add((double) index);
// dimension values
for (int i = 0; i < dimAttributesCount; i++) {
entry.add(example.getValue(dimAttributes[i]));
}
// add to stack
points.push(entry);
}
Stack<List<Double>> hull = this.buildHull(points);
for (List<Double> list : hull) {
exampleSet.getExample(list.get(0).intValue()).setValue(chMemberAttr, "true");
}
return new IOObject[] { exampleSet };
}
/************************************************************************************************
* PRIVATE METHODS
***********************************************************************************************/
private Stack<List<Double>> buildHull(Stack<List<Double>> rawPoints) {
/************************************************************************************************
* STEP 1
*
* The initial array of points is stored in the stack points. They are sorted according to
* their first dimension, which gives us the far left and far right points of the hull. These
* are special values, and they are stored off separately in the left and right value.
*
* Then we go through the list of points, and one by one determine whether each point is above
* or below the line formed by the right and left points. If it is above, the point is moved
* into the upperPartitionPoints sequence. If it is below, the point is moved into the
* lowerPartitionPoints sequence.
***********************************************************************************************/
List<Double> left = rawPoints.firstElement();
rawPoints.removeElement(rawPoints.firstElement());
List<Double> right = rawPoints.lastElement();
rawPoints.removeElement(rawPoints.lastElement());
Stack<List<Double>> upperPartitionPoints = new Stack<List<Double>>();
Stack<List<Double>> lowerPartitionPoints = new Stack<List<Double>>();
for (int i = 0; i < rawPoints.size(); i++) {
List<Double> point = rawPoints.get(i);
double dir = getDirection(left, right, point);
if (dir < 0) {
upperPartitionPoints.push(point);
}
else {
lowerPartitionPoints.push(point);
}
}
/************************************************************************************************
* STEP 2
*
* Building the hull consists of two procedures: building the lower and then the upper hull.
* The two procedures are nearly identical - the main difference between the two is the test
* for convexity. When building the upper hull, our rule is that the middle point must always
* be *above* the line formed by its two closest neighbors. When building the lower hull, the
* rule is that point must be *below* its two closest neighbors. We pass this information to
* the building routine as the last parameter, which is either -1 or 1.
***********************************************************************************************/
Stack<List<Double>> lowerHull = this.buildHalfHull(left, right, lowerPartitionPoints, 1);
Stack<List<Double>> upperHull = this.buildHalfHull(left, right, upperPartitionPoints, -1);
/*
* The convex hull is created, the lower hull and upper hull are stored in sorted sequences.
* There is a bit of duplication between the two, because both sets include the leftmost and
* rightmost point.
*/
Stack<List<Double>> hull = new Stack<List<Double>>();
// add all of the lower hull
hull.addAll(lowerHull);
// remove last one (the most right one)
hull.removeElement(hull.lastElement());
// add all but the most left one from the upper hull
upperHull.removeElement(upperHull.firstElement());
hull.addAll(upperHull);
return hull;
}
/**
* This is the method that builds either the upper or the lower half convex hull. It takes as its
* input a sorted list of points in one of the two halfs. It produces as output a list of the
* points in the corresponding convex hull.
*
* The factor should be 1 for the lower hull, and -1 for the upper hull.
*
* @param left
* @param right
* @param partitionPoints
* @param factor
* @return
*/
private Stack<List<Double>> buildHalfHull(List<Double> left, List<Double> right,
Stack<List<Double>> partitionPoints, int factor) {
Stack<List<Double>> halfHull = new Stack<List<Double>>();
/*
* The hull will always start with the left point, and end with the right point. According, we
* start by adding the left point as the first point in the output sequence, and make sure the
* right point is the last point in the input sequence.
*/
halfHull.push(left);
partitionPoints.push(right);
// The construction loop runs until the input is exhausted
while (partitionPoints.size() != 0) {
/*
* Repeatedly add the leftmost point to the hull, then test to see if a convexity violation
* has occurred. If it has, fix things up by removing the next-to-last point in the output
* sequence until convexity is restored.
*/
halfHull.push(partitionPoints.firstElement());
partitionPoints.removeElement(partitionPoints.firstElement());
while (halfHull.size() >= 3) {
int endPos = halfHull.size() - 1;
if (factor * this.getDirection(halfHull.get(endPos - 2), halfHull.get(endPos), halfHull.get(endPos - 1)) <= 0) {
halfHull.removeElement(halfHull.get(halfHull.indexOf(halfHull.firstElement()) + endPos - 1));
}
else {
break;
}
}
}
return halfHull;
}
/**
* In this program we frequently want to look at three consecutive points, p0, p1, and p2, and
* determine whether p2 has taken a turn to the left or a turn to the right.
*
* We can do this by by translating the points so that p0 is at the origin, then taking the cross
* product of p1 and p2. The result will be positive, negative, or 0, meaning respectively that
* p2 has turned right, left, or is on a straight line.
*
* {@link ls2-www.cs.uni-dortmund.de/lehre/winter200304/dap2ergseminar/vortr/alggeo.ppt}
*
* @param p0
* @param p1
* @param p2
* @return
*/
private double getDirection(List<Double> p0, List<Double> p1, List<Double> p2) {
// Matrix with p0-p1 and p2-p1 as columns
int rows = p0.size() - 1;
Matrix matrix = new Matrix(rows, 2);
for (int r = 0; r < rows; r++) {
matrix.set(r, 0, p0.get(r + 1) - p1.get(r + 1));
matrix.set(r, 1, p2.get(r + 1) - p1.get(r + 1));
}
return matrix.det();
}
}