Commit 723c098e authored by Nico Mack's avatar Nico Mack

Added leaders to ArcRangeGraph.

Introduced new VariableFormat utility class
parent 62273dfb
......@@ -110,7 +110,7 @@ public class NumericalVariable extends Variable<Double> {
return builder.toString();
}
public String formatValue() {
public String getFormattedValue() {
return format.format(value / scale);
}
......@@ -211,6 +211,22 @@ public class NumericalVariable extends Variable<Double> {
return this.maxValue;
}
/**
* Returns the normalized value of this variable if min and max value of variable have been
* defined, the plain value otherwise.
*
* @return the normalized value, i.e 0 <= x <= 1 of the variable.
*/
public double getNormalizedValue() {
double range = 0;
if ((minValue != -Double.MAX_VALUE) && (maxValue != Double.MAX_VALUE)) {
range = maxValue - minValue;
}
return (range > 0) ? (value - minValue) / range : value;
}
/**
*
* @param scale
......
/**
* Copyright Luxembourg Institute of Science and Technology, 2017. All rights reserved. If you wish
* to use this code for any purpose, please contact the author(s).
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package lu.list.itis.dkd.tui.utility;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.LineMetrics;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
/**
* @author mack
* @since [major].[minor]
* @version [major].[minor].[micro]
*/
// ***************************************************************************
// * Class Definition and Members *
// ***************************************************************************
public class Leader {
private double radialOffset;
private double leaderLineLength;
private double landingLineLength;
private Shape shape;
private Font labelFont;
private Color fillColour;
private Color strokeColour;
private Color labelColour;
private int strokeWidth;
private Stroke lineStroke;
private Line2D.Double leaderLine;
private Line2D.Double landingLine;
private String labelText;
private LineMetrics labelMetrics;
private Point labelPosition;
// ***************************************************************************
// * Constants *
// ***************************************************************************
private static final Point ZERO_ORIGIN = new Point(0, 0, 0, ScreenCoordinates.class);
// ---------------------------------------------------------------------------
// ***************************************************************************
// * Constructor(s) *
// ***************************************************************************
// ---------------------------------------------------------------------------
public Leader(double radialOffset, double leaderLineLength, double landingLineLength) {
this.radialOffset = radialOffset;
this.leaderLineLength = leaderLineLength;
this.landingLineLength = landingLineLength;
this.strokeWidth = 1;
this.lineStroke = new BasicStroke(strokeWidth);
this.strokeColour = Color.WHITE;
}
// ---------------------------------------------------------------------------
// ***************************************************************************
// * Class Body *
// ***************************************************************************
// ---------------------------------------------------------------------------
public void setFont(Font font) {
this.labelFont = font;
}
// ---------------------------------------------------------------------------
public void setLabelShape(Shape shape) {
this.shape = shape;
}
// ---------------------------------------------------------------------------
public void setFillColour(Color colour) {
this.fillColour = colour;
}
// ---------------------------------------------------------------------------
public void setStrokeColour(Color colour) {
this.strokeColour = colour;
}
// ---------------------------------------------------------------------------
public void setStrokeWidth(int width) {
this.strokeWidth = width;
this.lineStroke = new BasicStroke(this.strokeWidth);
}
// ---------------------------------------------------------------------------
public void setLabelColour(Color colour) {
this.labelColour = colour;
}
// ---------------------------------------------------------------------------
public void updateLeader(float leaderAngle, String label) {
float inner = (float) radialOffset;
float outer = (float) (radialOffset + leaderLineLength);
// Build Leader Line using polar coordinates
Point leaderStart = new Point(inner, inner, leaderAngle, ScreenCoordinates.class);
Point leaderEnd = new Point(outer, outer, leaderAngle, ScreenCoordinates.class);
Point start = PolarCoordinateHelper.polarToCarthesian(ZERO_ORIGIN, leaderStart);
Point end = PolarCoordinateHelper.polarToCarthesian(ZERO_ORIGIN, leaderEnd);
leaderLine = new Line2D.Double(new Point2D.Double(start.x, start.y), new Point2D.Double(end.x, end.y));
// Build Landing Line
int quadrant = PolarCoordinateHelper.getQuadrant(leaderAngle);
boolean flipped = ((quadrant == 1) || (quadrant == 2));
if (landingLineLength > 0) {
start = end.clone();
end.x += (flipped) ? -landingLineLength : landingLineLength;
landingLine = new Line2D.Double(new Point2D.Double(start.x, start.y), new Point2D.Double(end.x, end.y));
}
if (shape != null) {
end.x += (flipped) ? -shape.getBounds2D().getWidth() : 0;
}
labelPosition = end.clone();
labelText = label;
if (this.labelFont != null) {
labelMetrics = labelFont.getLineMetrics(this.labelText, new FontRenderContext(null, true, true));
}
}
// ---------------------------------------------------------------------------
public void paint(Graphics2D canvas) {
AffineTransform transform = canvas.getTransform();
Graphics2D localCanvas = (Graphics2D) canvas.create();
localCanvas.setTransform(transform);
localCanvas.setPaint(this.strokeColour);
localCanvas.setStroke(this.lineStroke);
localCanvas.draw(this.leaderLine);
localCanvas.draw(this.landingLine);
if (this.shape != null) {
transform.translate(labelPosition.x, labelPosition.y);
localCanvas.setTransform(transform);
localCanvas.draw(this.shape);
if (this.fillColour != null) {
localCanvas.setPaint(this.fillColour);
localCanvas.fill(this.shape);
}
}
if ((this.labelColour != null) && (this.labelText != null)) {
localCanvas.setPaint(labelColour);
localCanvas.setFont(labelFont);
localCanvas.drawString(labelText, 5, (labelMetrics.getAscent() / 2));
}
localCanvas.dispose();
}
}
/**
* Copyright Luxembourg Institute of Science and Technology, 2017. All rights reserved. If you wish
* to use this code for any purpose, please contact the author(s).
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package lu.list.itis.dkd.tui.utility;
import lu.list.itis.dkd.tui.cps.variable.Variable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author mack
* @since [major].[minor]
* @version [major].[minor].[micro]
*/
// ***************************************************************************
// * Class Definition and Members *
// ***************************************************************************
public class VariableFormat {
private List<Character> placeholders;
private String template;
// ***************************************************************************
// * Constants *
// ***************************************************************************
private static final char DISPLAY_NAME = 'd';
private static final char NAME = 'n';
private static final char VALUE = 'v';
private static final char FORMATTED_VALUE = 'f';
private static final char UNIT = 'u';
private static final char MIN_VALUE = 'm';
private static final char MAX_VALUE = 'x';
private static final char SCALE = 's';
private static final char TYPE = 't';
private static final Map<Character, String> VARIABLE_PROPERTIES = new HashMap<>();
static {
VARIABLE_PROPERTIES.put(DISPLAY_NAME, "DisplayName"); //$NON-NLS-1$
VARIABLE_PROPERTIES.put(NAME, "Name"); //$NON-NLS-1$
VARIABLE_PROPERTIES.put(VALUE, "Value"); //$NON-NLS-1$
VARIABLE_PROPERTIES.put(FORMATTED_VALUE, "FormattedValue"); //$NON-NLS-1$
VARIABLE_PROPERTIES.put(UNIT, "Unit"); //$NON-NLS-1$
VARIABLE_PROPERTIES.put(MIN_VALUE, "MinValue"); //$NON-NLS-1$
VARIABLE_PROPERTIES.put(MAX_VALUE, "MaxValue"); //$NON-NLS-1$
VARIABLE_PROPERTIES.put(SCALE, "Scale"); //$NON-NLS-1$
VARIABLE_PROPERTIES.put(TYPE, "Type"); //$NON-NLS-1$
}
private static final String PLACEHOLDER = "{}";
private static final Pattern VARIABLE_FORMAT_PATTERN = Pattern.compile("([dnfvumxst])", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
// private static final Pattern VARIABLE_FORMAT_PATTERN =
// Pattern.compile("([dnfumxst]|v(\\d+(\\.\\d+)?)?)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
// private static final Pattern NUMERIC_FORMAT_PATTERN = Pattern.compile("\\d+(\\.\\d+)?)",
// Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
private static final Logger LOGGER = LoggerFactory.getLogger(VariableFormat.class.getSimpleName());
// ---------------------------------------------------------------------------
// ***************************************************************************
// * Constructor(s)
// ***************************************************************************
// ---------------------------------------------------------------------------
public VariableFormat(String formatString) {
this.template = this.parseFormatString(formatString);
}
// ---------------------------------------------------------------------------
// ***************************************************************************
// * Primitive(s)
// ***************************************************************************
// ---------------------------------------------------------------------------
private String parseFormatString(String format) {
placeholders = new ArrayList<>();
StringBuilder templateBuilder = new StringBuilder();
Matcher formatMatcher = VARIABLE_FORMAT_PATTERN.matcher(format);
int position = 0;
while (formatMatcher.find()) {
templateBuilder.append(format.substring(position, formatMatcher.start()));
placeholders.add(formatMatcher.group(1).charAt(0));
templateBuilder.append(PLACEHOLDER);
position = formatMatcher.end();
}
templateBuilder.append(format.substring(position));
return templateBuilder.toString();
}
// ---------------------------------------------------------------------------
/**
* Returns the value of the specified property. The method relies on reflection to call the
* getter method for the specified property.
*
* @param p_Property
* specifies the name of the property to get value of.
* @return The return value of the getter method for the specified property, if available.
* Method returns <code>null</code> if either specified property is <code>null</code> or
* no getter exists.
*/
// ---------------------------------------------------------------------------
private Object getProperty(Variable<?> variable, String property) {
Class<?>[] parameterTypes = null;
Object[] parameters = null;
String methodName = ""; //$NON-NLS-1$
Method getter;
Object value = null;
try {
methodName = "get" + property; //$NON-NLS-1$
getter = variable.getClass().getMethod(methodName, parameterTypes);
value = getter.invoke(variable, parameters);
} catch (NoSuchMethodException exception) {
LOGGER.warn("Variable {} of type {} does not have a {} method!", variable.getName(), variable.getType(), methodName); //$NON-NLS-1$
return null;
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
LOGGER.error("Error while calling method {} on Variable {} of type {}!", methodName, variable.getName(), variable.getType(), e); //$NON-NLS-1$
}
return value;
}
// ---------------------------------------------------------------------------
// ***************************************************************************
// * Class Body
// ***************************************************************************
// ---------------------------------------------------------------------------
public String format(Variable<?> variable) {
int index = 0;
Object[] values = new Object[placeholders.size()];
for (Character placeholder : placeholders) {
values[index++] = getProperty(variable, VARIABLE_PROPERTIES.get(placeholder));
}
return StringUtils.build(this.template, values);
}
// ---------------------------------------------------------------------------
// ***************************************************************************
// * End of Class
// ***************************************************************************
// ---------------------------------------------------------------------------
}
package lu.list.itis.dkd.tui.widget.corona;
import lu.list.itis.dkd.tui.utility.ColorPair;
import lu.list.itis.dkd.tui.utility.Leader;
import lu.list.itis.dkd.tui.utility.Point;
import lu.list.itis.dkd.tui.utility.StringUtils;
import lu.list.itis.dkd.tui.utility.ScreenCoordinates;
import lu.list.itis.dkd.tui.utility.ValueRange;
import lu.list.itis.dkd.tui.utility.VariableFormat;
import lu.list.itis.dkd.tui.widget.corona.builder.ArcRangeGraphBuilder;
import org.pushingpixels.trident.Timeline;
......@@ -15,8 +17,6 @@ import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.LineMetrics;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
......@@ -49,6 +49,7 @@ public class ArcRangeGraph extends ValueRangeCorona {
private ColorPair strokeColour;
private ColorPair labelColour;
private ColorPair textColour;
private ColorPair leaderColour;
private ColorPair faceColour;
private ColorPair bezelColour;
......@@ -57,27 +58,41 @@ public class ArcRangeGraph extends ValueRangeCorona {
private Font textFont;
private Stroke borderStroke;
private int strokeWidth;
private String labelFormat;
private Area outer;
private Area inner;
private int leaderLineLength;
private int landingLineLength;
private Area outerArea;
private Area innerArea;
private Area positiveArc;
private Area negativeArc;
private Area face;
private String label;
private LineMetrics labelMetrics;
private Leader lowerLeader;
private Leader upperLeader;
private VariableFormat variableFormat;
private String lowerValueLabel;
private String upperValueLabel;
protected Timeline blinkingTimeline;
protected Timeline balisticTimeline;
private float opacity;
private float angle;
private float rotation;
private double shownLowerValue;
private double shownUpperValue;
private double referenceAngle;
private double lowerAngle;
private double upperAngle;
// ***************************************************************************
// * Constants *
// ***************************************************************************
private static final Point ZERO_ORIGIN = new Point(0, 0, 0, ScreenCoordinates.class);
// ---------------------------------------------------------------------------
// ***************************************************************************
// * Constructor(s) *
......@@ -101,11 +116,15 @@ public class ArcRangeGraph extends ValueRangeCorona {
this.strokeColour = builder.strokeColour;
this.labelColour = builder.labelColour;
this.textColour = builder.textColour;
this.leaderColour = builder.leaderColour;
this.faceColour = builder.faceColour;
this.bezelColour = builder.bezelColour;
this.labelShape = builder.labelShape;
this.textFont = builder.textFont;
this.strokeWidth = builder.strokeWidth;
this.landingLineLength = builder.landingLineLength;
this.leaderLineLength = builder.leaderLineLength;
this.labelFormat = builder.labelFormat;
this.buildFromProperties();
}
......@@ -134,11 +153,15 @@ public class ArcRangeGraph extends ValueRangeCorona {
this.strokeColour = original.strokeColour;
this.labelColour = original.labelColour;
this.textColour = original.textColour;
this.leaderColour = original.leaderColour;
this.faceColour = original.faceColour;
this.bezelColour = original.bezelColour;
this.labelShape = original.labelShape;
this.textFont = original.textFont;
this.strokeWidth = original.strokeWidth;
this.landingLineLength = original.landingLineLength;
this.leaderLineLength = original.leaderLineLength;
this.labelFormat = original.labelFormat;
this.buildFromProperties();
}
......@@ -153,15 +176,15 @@ public class ArcRangeGraph extends ValueRangeCorona {
double diameter;
diameter = 2 * innerRadius;
inner = new Area(new Ellipse2D.Double(-innerRadius, -innerRadius, diameter, diameter));
innerArea = new Area(new Ellipse2D.Double(-innerRadius, -innerRadius, diameter, diameter));
diameter = 2 * outerRadius;
outer = new Area(new Ellipse2D.Double(-outerRadius, -outerRadius, diameter, diameter));
outerArea = new Area(new Ellipse2D.Double(-outerRadius, -outerRadius, diameter, diameter));
Shape sector = new Arc2D.Double(-outerRadius, -outerRadius, diameter, diameter, startAngle, arcSpan, Arc2D.PIE);
face = new Area(sector);
face.subtract(inner);
outer.subtract(inner);
face.subtract(innerArea);
outerArea.subtract(innerArea);
if (this.labelShape != null) {
AffineTransform originTranslator = new AffineTransform();
......@@ -172,26 +195,28 @@ public class ArcRangeGraph extends ValueRangeCorona {
labelShape = originTranslator.createTransformedShape(labelShape);
}
if ((this.labelFormat != null) && (!this.labelFormat.isEmpty())) {
this.variableFormat = new VariableFormat(this.labelFormat);
} else {
this.variableFormat = new VariableFormat("d f u"); //$NON-NLS-1$
}
borderStroke = (strokeWidth > 0) ? new BasicStroke(strokeWidth) : null;
ValueRange range;
range = (relative) ? new ValueRange(this.reference, this.reference)
: new ValueRange(this.lowerBoundVariable.getValue(), this.upperBoundVariable.getValue());
this.setInformation(range);
}
// ---------------------------------------------------------------------------
private Point getOffsetFromCenter() {
double alpha = Math.toRadians(this.startAngle);
boolean clockwise = (this.arcSpan < 0);
double centerRadius = (this.innerRadius + this.outerRadius) / 2;
lowerLeader = new Leader(innerRadius, leaderLineLength, landingLineLength);
lowerLeader.setLabelShape(labelShape);
lowerLeader.setFont(textFont);
double offsetX = (5 + centerRadius * Math.cos(alpha)) * ((clockwise) ? 1 : -1);
double offsetY = (centerRadius * -Math.sin(alpha));
upperLeader = new Leader(innerRadius, leaderLineLength, landingLineLength);
upperLeader.setLabelShape(labelShape);
upperLeader.setFont(textFont);
return new Point((float) offsetX, (float) offsetY, 0);
this.setInformation(range);
}
// ---------------------------------------------------------------------------
......@@ -221,7 +246,8 @@ public class ArcRangeGraph extends ValueRangeCorona {
fillColour.setSwitched(isNegative);
strokeColour.setSwitched(isNegative);
labelColour.setSwitched(isNegative);
textColour.setSwitched(isNegative);
if (textColour != null)
textColour.setSwitched(isNegative);
if (faceColour != null)
faceColour.setSwitched(isNegative);
if (bezelColour != null)
......@@ -231,6 +257,17 @@ public class ArcRangeGraph extends ValueRangeCorona {
// ---------------------------------------------------------------------------
private void updateLeader() {
float lowerLeaderAngle = (float) -Math.toRadians(lowerAngle);
float upperLeaderAngle = (float) -Math.toRadians(upperAngle);
this.lowerLeader.updateLeader(lowerLeaderAngle, lowerValueLabel);
this.upperLeader.updateLeader(upperLeaderAngle, upperValueLabel);
}
// ---------------------------------------------------------------------------
private synchronized void updateArcFromValues(double lower, double upper) {
if ((lowerBoundVariable == null) || (upperBoundVariable == null)) {
......@@ -240,14 +277,18 @@ public class ArcRangeGraph extends ValueRangeCorona {
double extend = 0;