/*
* Copyright 2014-2016 Cel Skeggs.
*
* This file is part of the CCRE, the Common Chicken Runtime Engine.
*
* The CCRE is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* The CCRE is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the CCRE. If not, see <http://www.gnu.org/licenses/>.
*/
package ccre.supercanvas.components.channels;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import ccre.channel.CancelOutput;
import ccre.channel.FloatInput;
import ccre.channel.FloatOutput;
import ccre.log.Logger;
import ccre.rconf.RConf;
import ccre.rconf.RConf.Entry;
import ccre.supercanvas.BaseChannelComponent;
import ccre.supercanvas.Rendering;
import ccre.supercanvas.SuperCanvasPanel;
/**
* A component allowing display of floats.
*
* @author skeggsc
*/
public class FloatDisplayComponent extends BaseChannelComponent<FloatDisplayComponent.View> implements FloatOutput {
static enum View {
HORIZONTAL_POINTER, DIAL, TEXTUAL
}
private static final long serialVersionUID = 4027452153991095626L;
private float value;
private float minimum = -1.0f, maximum = 1.0f;
private boolean subscribed;
private final FloatInput inp;
// TODO: collapse subscribed and unsubscribe? here and other places?
private transient CancelOutput unsubscribe;
/**
* Create a new FloatDisplayComponent with a FloatInput to read from.
*
* @param cx the X coordinate.
* @param cy the Y coordinate.
* @param name the name of the input.
* @param inp the FloatInput to read from.
*/
public FloatDisplayComponent(int cx, int cy, String name, FloatInput inp) {
super(cx, cy, name);
this.inp = inp;
}
/**
* Create a new FloatDisplayComponent.
*
* @param cx the X coordinate.
* @param cy the Y coordinate.
* @param name the name of the input.
*/
public FloatDisplayComponent(int cx, int cy, String name) {
this(cx, cy, name, null);
}
@Override
public void channelRender(Graphics2D g, int screenWidth, int screenHeight, FontMetrics fontMetrics, int mouseX, int mouseY) {
switch (activeView) {
case HORIZONTAL_POINTER:
g.setColor(Color.WHITE);
g.fillRect(centerX - halfWidth + 10, centerY - halfHeight / 2, 2 * halfWidth - 20, halfHeight);
g.setColor(Color.BLACK);
g.drawRect(centerX - halfWidth + 10, centerY - halfHeight / 2, 2 * halfWidth - 21, halfHeight - 1);
g.drawLine(centerX, centerY + halfHeight / 2 - 1, centerX, centerY + 5);
g.drawLine(centerX + halfWidth * 2 / 3, centerY + halfHeight / 2 - 1, centerX + halfWidth * 2 / 3, centerY + 5);
g.drawLine(centerX - halfWidth * 2 / 3, centerY + halfHeight / 2 - 1, centerX - halfWidth * 2 / 3, centerY + 5);
g.drawLine(centerX - halfWidth / 6, centerY + halfHeight / 2 - 1, centerX - halfWidth / 6, centerY + 15);
g.drawLine(centerX + halfWidth / 6, centerY + halfHeight / 2 - 1, centerX + halfWidth / 6, centerY + 15);
g.drawLine(centerX - halfWidth / 3, centerY + halfHeight / 2 - 1, centerX - halfWidth / 3, centerY + 10);
g.drawLine(centerX + halfWidth / 3, centerY + halfHeight / 2 - 1, centerX + halfWidth / 3, centerY + 10);
g.drawLine(centerX - 3 * halfWidth / 6, centerY + halfHeight / 2 - 1, centerX - 3 * halfWidth / 6, centerY + 15);
g.drawLine(centerX + 3 * halfWidth / 6, centerY + halfHeight / 2 - 1, centerX + 3 * halfWidth / 6, centerY + 15);
if (value != 0) {
String strv = String.format("%.3f", value);
g.drawString(strv, value > 0 ? centerX - fontMetrics.stringWidth(strv) - 10 : centerX + 10, centerY - halfHeight / 2 + fontMetrics.getHeight());
}
int ptrCtr = (int) (centerX + halfWidth * ((2 * (value - minimum) / (maximum - minimum)) - 1) * 2 / 3);
if (minimum <= value && value <= maximum) {
if (value < 0) {
g.setColor(value == -1 ? Color.RED : Color.RED.darker().darker());
} else if (value > 0) {
g.setColor(value == 1 ? Color.GREEN : Color.GREEN.darker().darker());
} else {
g.setColor(Color.ORANGE);
}
}
Shape c = g.getClip();
g.setClip(new Rectangle(centerX - halfWidth + 10, centerY - halfHeight / 2, halfWidth * 2 - 20, halfHeight));
g.fillRect(ptrCtr - 5, centerY - halfHeight / 2 + 1, 11, halfHeight / 2 - 4);
g.fillPolygon(new int[] { ptrCtr - 5, ptrCtr, ptrCtr + 6 }, new int[] { centerY - 3, centerY + 3, centerY - 3 }, 3);
g.setClip(c);
break;
case DIAL:
g.setColor(Color.WHITE);
int rad = halfWidth - 8;
g.fillOval(centerX - halfWidth + 8, centerY - halfHeight + 20, rad * 2, rad * 2);
g.setColor(Color.BLACK);
g.drawOval(centerX - halfWidth + 8, centerY - halfHeight + 20, rad * 2, rad * 2);
g.setColor(Color.BLACK);
AffineTransform origT = g.getTransform();
g.translate(centerX, centerY - halfHeight + 20 + rad);
AffineTransform baseT = g.getTransform();
int eachSpoke = 14;
for (int i = -10 * eachSpoke; i <= 10 * eachSpoke; i += eachSpoke) {
g.setTransform(baseT);
g.rotate(Math.toRadians(i));
g.translate(0, 1 - rad);
if (i % (eachSpoke * 5) == 0) {
g.drawLine(0, 0, 0, rad / 3);
g.translate(0, rad / 3f);
g.rotate(Math.toRadians(i > 0 ? -90 : 90));
String str = Float.toString(((i / (10f * eachSpoke) + 1) / 2f) * (maximum - minimum) + minimum);
g.drawString(str, i > 0 ? -g.getFontMetrics().stringWidth(str) : 0, g.getFontMetrics().getDescent());
} else {
g.drawLine(0, 0, 0, rad / 6);
}
}
g.setTransform(baseT);
float angle = Math.max(-170, Math.min((((value - minimum) / (maximum - minimum)) * 2 - 1) * 10 * eachSpoke, 170));
g.setColor(Color.BLUE);
if (angle <= -170 || angle >= 170) {
g.setColor(Color.RED);
}
g.rotate(Math.toRadians(angle));
Stroke origS = g.getStroke();
g.setStroke(new BasicStroke(6, BasicStroke.CAP_ROUND, BasicStroke.CAP_SQUARE));
g.drawLine(0, -rad / 2, 0, 0);
g.setStroke(origS);
g.setTransform(origT);
break;
case TEXTUAL:
g.setColor(Color.BLACK);
g.setFont(Rendering.labels);
String text;
if (value < 10 && value != 0) {
text = Float.toString(value);
String orig = text;
while (text.length() > 3 && g.getFontMetrics().stringWidth(text) > 2 * halfWidth) {
String nline = text.substring(0, text.length() - 1);
try {
if (Float.parseFloat(nline) == 0) {
break;
}
} catch (NumberFormatException ex) {
Logger.warning("Unexpected failure of number formatting", ex);
break;
}
text = nline;
}
if (g.getFontMetrics().stringWidth(text) > 2 * halfWidth) {
g.setFont(Rendering.console);
text = orig;
}
} else {
text = String.format("%.3f", value);
}
g.drawString(text, centerX - g.getFontMetrics().stringWidth(text) / 2, centerY + 5);
break;
}
}
@Override
public boolean onInteract(int x, int y) {
return false;
}
@Override
protected void onChangePanel(SuperCanvasPanel panel) {
boolean hasPanel = panel != null;
if (inp != null && hasPanel != subscribed) {
if (unsubscribe != null) {
unsubscribe.cancel();
unsubscribe = null;
}
if (hasPanel) {
unsubscribe = inp.send(this);
}
subscribed = hasPanel;
}
}
@Override
public void set(float value) {
this.value = value;
}
@Override
protected void setDefaultView() {
activeView = View.TEXTUAL;
}
@Override
public Entry[] queryRConf() throws InterruptedException {
return rconfBase(RConf.string("minimum"), RConf.fieldFloat(minimum), RConf.string("maximum"), RConf.fieldFloat(maximum));
}
@Override
public boolean signalRConf(int field, byte[] data) throws InterruptedException {
switch (rconfBase(field, data)) {
case 1:
minimum = RConf.bytesToFloat(data);
return true;
case 3:
maximum = RConf.bytesToFloat(data);
return true;
case BASE_VALID:
return true;
default:
return false;
}
}
}