/**
* Copyright (C) 2009-2014 Cars and Tracks Development Project (CTDP).
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package net.ctdp.rfdynhud.widgets.base.needlemeter;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import net.ctdp.rfdynhud.gamedata.LiveGameData;
import net.ctdp.rfdynhud.properties.BackgroundProperty;
import net.ctdp.rfdynhud.properties.BooleanProperty;
import net.ctdp.rfdynhud.properties.ColorProperty;
import net.ctdp.rfdynhud.properties.FactoredFloatProperty;
import net.ctdp.rfdynhud.properties.FloatProperty;
import net.ctdp.rfdynhud.properties.FontProperty;
import net.ctdp.rfdynhud.properties.ImageProperty;
import net.ctdp.rfdynhud.properties.IntProperty;
import net.ctdp.rfdynhud.properties.Property;
import net.ctdp.rfdynhud.properties.PropertyLoader;
import net.ctdp.rfdynhud.properties.PropertiesContainer;
import net.ctdp.rfdynhud.render.DrawnString;
import net.ctdp.rfdynhud.render.DrawnString.Alignment;
import net.ctdp.rfdynhud.render.DrawnStringFactory;
import net.ctdp.rfdynhud.render.ImageTemplate;
import net.ctdp.rfdynhud.render.Texture2DCanvas;
import net.ctdp.rfdynhud.render.TextureImage2D;
import net.ctdp.rfdynhud.render.TransformableTexture;
import net.ctdp.rfdynhud.util.FontUtils;
import net.ctdp.rfdynhud.util.RFDHLog;
import net.ctdp.rfdynhud.util.SubTextureCollector;
import net.ctdp.rfdynhud.util.PropertyWriter;
import net.ctdp.rfdynhud.valuemanagers.Clock;
import net.ctdp.rfdynhud.values.IntValue;
import net.ctdp.rfdynhud.widgets.base.widget.Widget;
import net.ctdp.rfdynhud.widgets.base.widget.WidgetPackage;
import net.ctdp.rfdynhud.widgets.base.widget.WidgetSet;
/**
* The {@link NeedleMeterWidget} is an abstract {@link Widget} implementation
* for meter widgets with a needle on an analogue scale.
*
* @author Marvin Froehlich (CTDP)
*/
public abstract class NeedleMeterWidget extends Widget
{
public static final int NEEDLE_LOCAL_Z_INDEX = 1000;
@Override
protected String getInitialBackground()
{
return ( BackgroundProperty.IMAGE_INDICATOR + "standard/rev_meter_bg.png" );
}
@Override
protected void onBackgroundChanged( boolean imageChanged, float deltaScaleX, float deltaScaleY )
{
super.onBackgroundChanged( imageChanged, deltaScaleX, deltaScaleY );
if ( deltaScaleX > 0f )
{
markersInnerRadius.setIntValue( Math.round( markersInnerRadius.getIntValue() * deltaScaleX ) );
markersLength.setIntValue( Math.round( markersLength.getIntValue() * ( deltaScaleX + deltaScaleY ) / 2 ) );
valuePosX.setIntValue( Math.round( valuePosX.getIntValue() * deltaScaleX ) );
valuePosY.setIntValue( Math.round( valuePosY.getIntValue() * deltaScaleY ) );
}
}
protected static final float MIN_MAX_VALUE_NONE = 1000000000f;
protected final FloatProperty minValue = new FloatProperty( "minValue", -MIN_MAX_VALUE_NONE );
protected final FloatProperty maxValue = new FloatProperty( "maxValue", +MIN_MAX_VALUE_NONE );
protected final BooleanProperty displayMarkers = new BooleanProperty( "displayMarkers", true );
protected final BooleanProperty displayMarkerNumbers = new BooleanProperty( "displayMarkerNumbers", "displayNumbers", true );
protected final BooleanProperty markerNumbersInside = new BooleanProperty( "markerNumbersInside", "numbersInside", false );
protected final IntProperty markersInnerRadius = new IntProperty( "markersInnerRadius", "innerRadius", 224, 1, Integer.MAX_VALUE, false );
protected final IntProperty markersLength = new IntProperty( "markersLength", "length", 50, 4, Integer.MAX_VALUE, false );
protected final BooleanProperty markersOnCircle = new BooleanProperty( "markersOnCircle", true );
protected final FactoredFloatProperty firstMarkerNumberOffset = new FactoredFloatProperty( "firstMarkerNumberOffset", "firstNumberOffset", FactoredFloatProperty.FACTOR_DEGREES_TO_RADIANS, 0f, -360.0f, +360.0f );
protected final FactoredFloatProperty lastMarkerNumberOffset = new FactoredFloatProperty( "lastMarkerNumberOffset", "lastNumberOffset", FactoredFloatProperty.FACTOR_DEGREES_TO_RADIANS, 0f, -360.0f, +360.0f );
protected int getMarkersBigStepLowerLimit()
{
return ( 300 );
}
protected final IntProperty markersBigStep = new IntProperty( "markersBigStep", "bigStep", 1000, getMarkersBigStepLowerLimit(), Integer.MAX_VALUE, false )
{
@Override
protected void onValueChanged( Integer oldValue, int newValue )
{
fixSmallStep();
}
};
protected int getMarkersSmallStepLowerLimit()
{
return ( 20 );
}
protected final IntProperty markersSmallStep = new IntProperty( "markersSmallStep", "smallStep", 200, getMarkersSmallStepLowerLimit(), Integer.MAX_VALUE, false )
{
@Override
protected void onValueChanged( Integer oldValue, int newValue )
{
fixSmallStep();
}
};
protected final BooleanProperty lastMarkerBig = new BooleanProperty( "lastMarkerBig", false );
protected final ColorProperty markersColor = new ColorProperty( "markersColor", "color", "#FFFFFF" );
protected final FontProperty markersFont = new FontProperty( "markersFont", "font", "Monospaced" + FontUtils.SEPARATOR + "BOLD" + FontUtils.SEPARATOR + "9va" );
protected final ColorProperty markersFontColor = new ColorProperty( "markersFontColor", "fontColor", "#FFFFFF" );
protected final ColorProperty markersFontDropShadowColor = new ColorProperty( "markersFontDropShadowColor", "fontDropShadowColor", "#00000000" );
protected final BooleanProperty markerNumbersCentered = new BooleanProperty( "markerNumbersCentered", "numbersCentered", false );
protected void onNeedleImageNameChanged() {}
private TransformableTexture needleTexture = null;
protected String getInitialNeedleImage()
{
return ( "standard/rev_meter_needle.png" );
}
private final ImageProperty needleImageName = new ImageProperty( "needleImageName", "imageName", getInitialNeedleImage(), false, true )
{
@Override
protected void onValueChanged( String oldValue, String newValue )
{
onNeedleImageNameChanged();
}
};
protected final IntProperty needleMountX = new IntProperty( "needleMountX", -1, -1, 5000 );
protected final IntProperty needleMountY = new IntProperty( "needleMountY", -1, -1, 5000 );
protected final IntProperty needlePivotBottomOffset = new IntProperty( "needlePivotBottomOffset", "pivotBottomOffset", 60 );
protected final FactoredFloatProperty needleRotationForMinValue = new FactoredFloatProperty( "needleRotationForMinValue", "rotForMin", FactoredFloatProperty.FACTOR_DEGREES_TO_RADIANS, -122.4f, -360.0f, +360.0f );
protected final FactoredFloatProperty needleRotationForMaxValue = new FactoredFloatProperty( "needleRotationForMaxValue", "rotForMax", FactoredFloatProperty.FACTOR_DEGREES_TO_RADIANS, +118.8f, -360.0f, +360.0f );
private Boolean drawNeeldeMount = null;
protected final BooleanProperty displayValue = new BooleanProperty( "displayValue", true );
protected final ImageProperty valueBackgroundImageName = new ImageProperty( "valueBackgroundImageName", "backgroundImage", "standard/cyan_circle.png", false, true );
private TransformableTexture valueBackgroundTexture = null;
private TextureImage2D valueBackgroundTexture_bak = null;
protected final IntProperty valuePosX = new IntProperty( "valuePosX", "posX", 100 );
protected final IntProperty valuePosY = new IntProperty( "valuePosY", "posY", 100 );
private int valueBackgroundTexPosX, valueBackgroundTexPosY;
protected final FontProperty valueFont = new FontProperty( "valueFont", "font", FontProperty.STANDARD_FONT.getKey() );
protected final ColorProperty valueFontColor = new ColorProperty( "valueFontColor", "fontColor", "#1A261C" );
private DrawnString valueString = null;
private final IntValue valueValue = new IntValue();
@Override
public void onPropertyChanged( Property property, Object oldValue, Object newValue )
{
super.onPropertyChanged( property, oldValue, newValue );
if ( ( oldValue != null ) && ( ( property == needleMountX ) || ( property == needleMountY ) ) )
{
drawNeeldeMount = true;
}
}
private void fixSmallStep()
{
this.markersSmallStep.setIntValue( markersBigStep.getIntValue() / Math.round( (float)markersBigStep.getIntValue() / (float)markersSmallStep.getIntValue() ) );
}
public void setDisplayValue( boolean display )
{
this.displayValue.setBooleanValue( display );
}
public boolean getDisplayValue()
{
return ( displayValue.getBooleanValue() );
}
protected boolean getDisplayMarkers()
{
return ( displayMarkers.getBooleanValue() );
}
protected boolean getDisplayMarkerNumbers()
{
return ( displayMarkerNumbers.getBooleanValue() );
}
protected boolean getMarkerNumbersInside()
{
return ( markerNumbersInside.getBooleanValue() );
}
protected int getMarkersInnerRadius()
{
return ( markersInnerRadius.getIntValue() );
}
protected int getMarkersLength()
{
return ( markersLength.getIntValue() );
}
protected boolean getMarkersOnCircle()
{
return ( markersOnCircle.getBooleanValue() );
}
protected ImageTemplate getNeedleImage()
{
return ( needleImageName.getImage() );
}
protected final TransformableTexture getNeedleTexture()
{
return ( needleTexture );
}
protected int getNeedlePivotBottomOffset()
{
return ( needlePivotBottomOffset.getIntValue() );
}
protected int getNeedleMountX( int widgetWidth )
{
if ( needleMountX.getIntValue() < 0 )
return ( widgetWidth / 2 );
return ( Math.round( needleMountX.getIntValue() * getBackground().getScaleX() ) );
}
protected int getNeedleMountY( int widgetHeight )
{
if ( needleMountY.getIntValue() < 0 )
return ( widgetHeight / 2 );
return ( Math.round( needleMountY.getIntValue() * getBackground().getScaleY() ) );
}
protected float getNeedleRotationForMinValue()
{
return ( needleRotationForMinValue.getFactoredValue() );
}
protected float getNeedleRotationForMaxValue()
{
return ( needleRotationForMaxValue.getFactoredValue() );
}
//protected
protected ImageTemplate getValueBackgroundImage()
{
return ( valueBackgroundImageName.getImage() );
}
/*
protected TextureImage2D getValueBackgroundTexture()
{
return ( valueBackgroundTexture_bak );
}
*/
protected int getValuePosX()
{
return ( valuePosX.getIntValue() );
}
protected int getValuePosY()
{
return ( valuePosY.getIntValue() );
}
/**
* Gets the {@link FontProperty} for the value.
*
* @return the {@link FontProperty} for the value.
*/
protected FontProperty getValueFont()
{
return ( valueFont );
}
/**
* Gets the {@link ColorProperty} for the value.
*
* @return the {@link ColorProperty} for the value.
*/
protected ColorProperty getValueFontColor()
{
return ( valueFontColor );
}
private boolean loadValueBackgroundTexture( boolean isEditorMode )
{
if ( !displayValue.getBooleanValue() )
{
valueBackgroundTexture = null;
valueBackgroundTexture_bak = null;
return ( false );
}
try
{
ImageTemplate it = valueBackgroundImageName.getImage();
if ( it == null )
{
valueBackgroundTexture = null;
valueBackgroundTexture_bak = null;
return ( false );
}
float scale = getBackground().getScaleX();
int w = Math.round( it.getBaseWidth() * scale );
int h = Math.round( it.getBaseHeight() * scale );
if ( ( valueBackgroundTexture == null ) || ( valueBackgroundTexture.getWidth() != w ) || ( valueBackgroundTexture.getHeight() != h ) )
{
valueBackgroundTexture_bak = it.getScaledTextureImage( w, h, valueBackgroundTexture_bak, isEditorMode );
valueBackgroundTexture = TransformableTexture.getOrCreate( w, h, TransformableTexture.DEFAULT_PIXEL_PERFECT_POSITIONING, valueBackgroundTexture, isEditorMode );
valueBackgroundTexture.setDynamic( true );
valueBackgroundTexture.getTexture().clear( valueBackgroundTexture_bak, true, null );
forceAndSetDirty( false );
}
}
catch ( Throwable t )
{
log( t );
return ( false );
}
return ( true );
}
private boolean loadNeedleTexture( boolean isEditorMode )
{
if ( needleImageName.isNoImage() )
{
needleTexture = null;
return ( false );
}
try
{
ImageTemplate it = needleImageName.getImage();
if ( it == null )
{
needleTexture = null;
return ( false );
}
float scale = getBackground().getScaleX();
int w = Math.round( it.getBaseWidth() * scale );
int h = Math.round( it.getBaseHeight() * scale );
needleTexture = it.getScaledTransformableTexture( w, h, needleTexture, isEditorMode );
needleTexture.setLocalZIndex( NEEDLE_LOCAL_Z_INDEX );
}
catch ( Throwable t )
{
log( t );
return ( false );
}
return ( true );
}
/**
* {@inheritDoc}
*/
@Override
protected void initSubTextures( LiveGameData gameData, boolean isEditorMode, int widgetInnerWidth, int widgetInnerHeight, SubTextureCollector collector )
{
if ( loadValueBackgroundTexture( isEditorMode ) )
collector.add( valueBackgroundTexture );
if ( loadNeedleTexture( isEditorMode ) )
collector.add( needleTexture );
}
/**
* {@inheritDoc}
*/
@Override
public void onCockpitEntered( LiveGameData gameData, boolean isEditorMode )
{
super.onCockpitEntered( gameData, isEditorMode );
valueValue.reset();
}
/**
* {@inheritDoc}
*/
@Override
public void onVehicleSetupUpdated( LiveGameData gameData, boolean isEditorMode )
{
super.onVehicleSetupUpdated( gameData, isEditorMode );
forceCompleteRedraw( true );
forceReinitialization();
}
/**
* {@inheritDoc}
*/
@Override
public void onNeededDataComplete( LiveGameData gameData, boolean isEditorMode )
{
super.onNeededDataComplete( gameData, isEditorMode );
valueValue.reset();
}
/**
* {@inheritDoc}
*/
@Override
protected void initialize( LiveGameData gameData, boolean isEditorMode, DrawnStringFactory dsf, TextureImage2D texture, int width, int height )
{
final float backgroundScaleX = getBackground().getScaleX();
final float backgroundScaleY = getBackground().getScaleY();
if ( needleTexture != null )
{
int mountX = getNeedleMountX( width );
int mountY = getNeedleMountY( height );
needleTexture.setTranslation( mountX - needleTexture.getWidth() / 2, mountY - needleTexture.getHeight() + needlePivotBottomOffset.getIntValue() * backgroundScaleX );
needleTexture.setRotationCenter( needleTexture.getWidth() / 2, (int)( needleTexture.getHeight() - needlePivotBottomOffset.getIntValue() * backgroundScaleX ) );
}
if ( displayValue.getBooleanValue() )
{
FontProperty valueFont = getValueFont();
ColorProperty valueFontColor = getValueFontColor();
FontMetrics metrics = valueFont.getMetrics();
//Rectangle2D bounds = metrics.getStringBounds( "000", texture.getTextureCanvas() );
//double fw = bounds.getWidth();
double fh = metrics.getAscent() - metrics.getDescent();
int fx, fy;
if ( valueBackgroundTexture == null )
{
fx = Math.round( valuePosX.getIntValue() * backgroundScaleX );
fy = Math.round( valuePosY.getIntValue() * backgroundScaleY );
}
else
{
valueBackgroundTexPosX = Math.round( valuePosX.getIntValue() * backgroundScaleX - valueBackgroundTexture.getWidth() / 2.0f );
valueBackgroundTexPosY = Math.round( valuePosY.getIntValue() * backgroundScaleY - valueBackgroundTexture.getHeight() / 2.0f );
fx = valueBackgroundTexture.getWidth() / 2;
fy = valueBackgroundTexture.getHeight() / 2;
}
valueString = dsf.newDrawnString( "valueString", fx/* - (int)( fw / 2.0 )*/, fy - (int)( metrics.getDescent() + fh / 2.0 ), Alignment.LEFT, false, valueFont.getFont(), valueFont.isAntiAliased(), valueFontColor.getColor() );
}
else
{
valueBackgroundTexture = null;
valueBackgroundTexture_bak = null;
}
}
/**
* Gets the value for the needle and the digital value display.
* Override {@link #getValueForValueDisplay(LiveGameData, boolean)} to use a different value
* for the digital value.
*
* @param gameData
* @param isEditorMode
*
* @return the value for the needle and the digital value display.
*/
protected abstract float getValue( LiveGameData gameData, boolean isEditorMode );
/**
* Gets the value for the digital value display.
* The default implementation simply gets the result of {@link #getValue(LiveGameData, boolean)} and converts it to an int.
*
* @param gameData
* @param isEditorMode
*
* @return the value for the digital value display.
*/
protected int getValueForValueDisplay( LiveGameData gameData, boolean isEditorMode)
{
return ( Math.round( getValue( gameData, isEditorMode ) ) );
}
/**
* Gets the minimum value for the markers and needle coming from game data or known limits.
*
* @param gameData
* @param isEditorMode
*
* @return the minimum value for the markers and needle.
*/
protected abstract float getMinDataValue( LiveGameData gameData, boolean isEditorMode );
/**
* Gets the maximum value for the markers and needle coming from game data or known limits.
*
* @param gameData
* @param isEditorMode
*
* @return the maximum value for the markers and needle.
*/
protected abstract float getMaxDataValue( LiveGameData gameData, boolean isEditorMode );
/**
* Gets the minimum value for the markers and needle.
* If the minValue property is set to a valid value, the value is returned, otherwise the result of {@link #getMinDataValue(LiveGameData, boolean)} is returned.
*
* @param gameData
* @param isEditorMode
*
* @return the minimum value for the markers and needle.
*/
protected final float getMinValue( LiveGameData gameData, boolean isEditorMode )
{
float minDataValue = getMinDataValue( gameData, isEditorMode );
if ( minValue.getFloatValue() > minDataValue )
return ( minValue.getFloatValue() );
return ( minDataValue );
}
/**
* Gets the maximum value for the markers and needle.
* If the maxValue property is set to a valid value, the value is returned, otherwise the result of {@link #getMaxDataValue(LiveGameData, boolean)} is returned.
*
* @param gameData
* @param isEditorMode
*
* @return the maximum value for the markers and needle.
*/
protected final float getMaxValue( LiveGameData gameData, boolean isEditorMode )
{
float maxDataValue = getMaxDataValue( gameData, isEditorMode );
if ( maxValue.getFloatValue() < maxDataValue )
return ( maxValue.getFloatValue() );
return ( maxDataValue );
}
/**
* Gets, whether the needle may go below the {@link #getMinValue(LiveGameData, boolean)} result.
* The default implementation returns <code>false</code>.
*
* @return whether the needle may go below the {@link #getMinValue(LiveGameData, boolean)} result.
*/
protected boolean getNeedleMayExceedMinimum()
{
return ( false );
}
/**
* Gets, whether the needle may go beyond the {@link #getMaxValue(LiveGameData, boolean)} result.
* The default implementation returns <code>true</code>.
*
* @return whether the needle may go beyond the {@link #getMaxValue(LiveGameData, boolean)} result.
*/
protected boolean getNeedleMayExceedMaximum()
{
return ( true );
}
/**
* Gets the text label for the big markers at the given value.
*
* @param gameData
* @param isEditorMode
* @param value
*
* @return the text label for the big markers at the given value.
*/
protected abstract String getMarkerLabelForValue( LiveGameData gameData, boolean isEditorMode, float value );
/**
*
* @param gameData
* @param isEditorMode
* @param texCanvas
* @param offsetX
* @param offsetY
* @param width
* @param height
* @param innerRadius
* @param bigOuterRadius
* @param smallOuterRadius
*/
protected void prepareMarkersBackground( LiveGameData gameData, boolean isEditorMode, Texture2DCanvas texCanvas, int offsetX, int offsetY, int width, int height, float innerRadius, float bigOuterRadius, float smallOuterRadius )
{
}
/**
* Gets a certain marker's color at the given value.
*
* @param gameData
* @param isEditorMode
* @param value
* @param minValue
* @param maxValue
*
* @return a certain marker's color at the given value.
*/
protected Color getMarkerColorForValue( LiveGameData gameData, boolean isEditorMode, int value, int minValue, int maxValue )
{
return ( markersColor.getColor() );
}
/**
* Gets a certain marker number's color at the given value.
*
* @param gameData
* @param isEditorMode
* @param value
* @param minValue
* @param maxValue
*
* @return a certain marker's color at the given value.
*/
protected Color getMarkerNumberColorForValue( LiveGameData gameData, boolean isEditorMode, int value, int minValue, int maxValue )
{
return ( markersFontColor.getColor() );
}
/**
* Draws the markers.
*
* @param gameData
* @param isEditorMode
* @param texCanvas
* @param offsetX
* @param offsetY
* @param width
* @param height
*/
protected void drawMarkers( LiveGameData gameData, boolean isEditorMode, Texture2DCanvas texCanvas, int offsetX, int offsetY, int width, int height )
{
final boolean dm = getDisplayMarkers();
final boolean dmn = getDisplayMarkerNumbers();
final boolean mni = getMarkerNumbersInside();
final float backgroundScaleX = getBackground().getScaleX();
//final float backgroundScaleY = getBackground().getBackgroundScaleY();
int minValue = (int)getMinValue( gameData, isEditorMode );
int maxValue = (int)getMaxValue( gameData, isEditorMode );
float range = ( maxValue - minValue );
if ( minValue < 0 )
{
RFDHLog.error( "Wrong data: min RPM is less than 0. Assuming 0." );
minValue = 0;
}
if ( maxValue > 100000 )
{
RFDHLog.error( "Wrong data: max RPM is greater than 100.000. Assuming 100.000." );
maxValue = 100000;
}
final float centerX = offsetX + getNeedleMountX( width );
final float centerY = offsetY + getNeedleMountY( height );
final float innerAspect = markersOnCircle.getBooleanValue() ? 1.0f : getInnerSize().getAspect();
texCanvas.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
float innerRadius = markersInnerRadius.getIntValue() * backgroundScaleX;
//float markersLength2 = markersLength.getIntValue() * backgroundScaleX;
float bigOuterRadius0 = ( markersInnerRadius.getIntValue() + markersLength.getIntValue() - 1 ) * backgroundScaleX;
float bigOuterRadius = ( markersInnerRadius.getIntValue() + ( dm ? markersLength.getIntValue() - 1 : 0 ) ) * backgroundScaleX;
float smallOuterRadius0 = innerRadius + ( bigOuterRadius0 - innerRadius ) * 0.75f;
float smallOuterRadius = innerRadius + ( bigOuterRadius - innerRadius ) * 0.75f;
prepareMarkersBackground( gameData, isEditorMode, texCanvas, offsetX, offsetY, width, height, innerRadius, bigOuterRadius, smallOuterRadius0 );
Stroke oldStroke = texCanvas.getStroke();
Stroke bigStroke = new BasicStroke( 2 );
Stroke smallStroke = new BasicStroke( 1 );
FontProperty numberFont = markersFont;
texCanvas.setFont( numberFont.getFont() );
FontMetrics metrics = numberFont.getMetrics();
AffineTransform at0 = new AffineTransform( texCanvas.getTransform() );
AffineTransform at1 = new AffineTransform();
AffineTransform at2 = new AffineTransform();
AffineTransform atCenterTranslate = AffineTransform.getTranslateInstance( centerX, centerY );
AffineTransform atEllipticScale = AffineTransform.getScaleInstance( 1.0, 1.0 / innerAspect );
Point2D.Double p0 = new Point2D.Double();
/*
String biggestString = String.valueOf( getMarkerLabelForValue( gameData, isEditorMode, Math.max( minValue, maxValue ) ) );
Rectangle2D biggestBounds = metrics.getStringBounds( biggestString, texCanvas );
double maxFW = biggestBounds.getWidth();
double maxFH = biggestBounds.getHeight();
//double maxFH = metrics.getAscent() - metrics.getDescent();
double maxOff = Math.sqrt( maxFW * maxFW + maxFH * maxFH ) / 2.0;
*/
Color dropShadowColor = markersFontDropShadowColor.getColor();
float dropShadowOffset = 2.2f; //numberFont.getFont().getSize() * 0.2f;
boolean drawDropShadow = ( dropShadowColor.getAlpha() > 0 );
double equalOff = 0.0;
if ( markerNumbersCentered.getBooleanValue() )
{
String biggestString = String.valueOf( getMarkerLabelForValue( gameData, isEditorMode, Math.max( minValue, maxValue ) ) );
Rectangle2D bounds = metrics.getStringBounds( biggestString, texCanvas );
double fw = bounds.getWidth();
//double fh = metrics.getAscent() - metrics.getDescent();
double fh = bounds.getHeight();
equalOff = Math.sqrt( fw * fw + fh * fh ) / 2.0;
}
final int smallStep = markersSmallStep.getIntValue();
for ( int value = minValue; value <= maxValue; value += smallStep )
{
float angle = +( needleRotationForMinValue.getFactoredValue() + ( needleRotationForMaxValue.getFactoredValue() - needleRotationForMinValue.getFactoredValue() ) * ( ( value - minValue ) / range ) );
if ( value == minValue )
angle += firstMarkerNumberOffset.getFactoredValue();
else if ( value + markersBigStep.getIntValue() > maxValue )
angle += lastMarkerNumberOffset.getFactoredValue();
at1.setTransform( atCenterTranslate );
at2.setTransform( atEllipticScale );
at1.concatenate( at2 );
at2.setToRotation( angle );
at1.concatenate( at2 );
at2.setToTranslation( 0, -innerRadius );
at1.concatenate( at2 );
p0.setLocation( 0, 0 );
at1.transform( p0, p0 );
double vecX = p0.x - centerX;
double vecY = p0.y - centerY;
double len = Math.sqrt( vecX * vecX + vecY * vecY );
vecX /= len;
vecY /= len;
texCanvas.setColor( getMarkerColorForValue( gameData, isEditorMode, value, minValue, maxValue ) );
if ( ( ( value % markersBigStep.getIntValue() ) == 0 ) || ( lastMarkerBig.getBooleanValue() && ( value + smallStep > maxValue ) ) )
{
if ( dm )
{
texCanvas.setStroke( bigStroke );
float l = bigOuterRadius - innerRadius;
texCanvas.drawLine( (int)Math.round( p0.x ), (int)Math.round( p0.y ), (int)Math.round( p0.x + vecX * l ), (int)Math.round( p0.y + vecY * l ) );
}
if ( dmn )
{
String s = getMarkerLabelForValue( gameData, isEditorMode, value );
if ( s != null )
{
Rectangle2D bounds = metrics.getStringBounds( s, texCanvas );
double fw = bounds.getWidth();
//double fh = metrics.getAscent() - metrics.getDescent();
double fh = bounds.getHeight();
double off = markerNumbersCentered.getBooleanValue() ? equalOff : Math.sqrt( fw * fw + fh * fh ) / 2.0;
int x, y;
if ( mni )
{
x = (int)Math.round( p0.x + vecX * ( -off - 1 ) );
y = (int)Math.round( p0.y + vecY * ( -off - 1 ) );
}
else
{
x = (int)Math.round( p0.x + vecX * ( bigOuterRadius0 - innerRadius + off + 1 ) );
y = (int)Math.round( p0.y + vecY * ( bigOuterRadius0 - innerRadius + off + 1 ) );
}
if ( drawDropShadow )
{
texCanvas.setColor( dropShadowColor );
texCanvas.drawString( s, x - (int)( fw / 2 ) + dropShadowOffset, y + (int)( -fh / 2 - bounds.getY() ) + dropShadowOffset );
}
texCanvas.setColor( getMarkerNumberColorForValue( gameData, isEditorMode, value, minValue, maxValue ) );
texCanvas.drawString( s, x - (int)( fw / 2 ), y + (int)( -fh / 2 - bounds.getY() ) );
/*
texCanvas.setColor( Color.GREEN );
texCanvas.setStroke( smallStroke );
texCanvas.drawLine( x - 2, y - 2, x + 2, y + 2 );
texCanvas.drawLine( x - 2, y + 2, x + 2, y - 2 );
*/
}
}
}
else if ( dm )
{
texCanvas.setStroke( smallStroke );
float l = smallOuterRadius - innerRadius;
texCanvas.drawLine( (int)Math.round( p0.x ), (int)Math.round( p0.y ), (int)Math.round( p0.x + vecX * l ), (int)Math.round( p0.y + vecY * l ) );
}
}
texCanvas.setTransform( at0 );
texCanvas.setStroke( oldStroke );
texCanvas.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_DEFAULT );
}
@Override
protected void drawBackground( LiveGameData gameData, boolean isEditorMode, TextureImage2D texture, int offsetX, int offsetY, int width, int height, boolean isRoot )
{
super.drawBackground( gameData, isEditorMode, texture, offsetX, offsetY, width, height, isRoot );
if ( getDisplayMarkers() || getDisplayMarkerNumbers() )
{
drawMarkers( gameData, isEditorMode, texture.getTextureCanvas(), offsetX, offsetY, width, height );
}
}
/**
* Live-checks, whether the needle is to be rendered or not.
*
* @param gameData
* @param isEditorMode
*
* @return whether to render the needle or not.
*/
protected boolean doRenderNeedle( LiveGameData gameData, boolean isEditorMode )
{
return ( true );
}
@Override
protected void drawWidget( Clock clock, boolean needsCompleteRedraw, LiveGameData gameData, boolean isEditorMode, TextureImage2D texture, int offsetX, int offsetY, int width, int height )
{
if ( needleTexture != null )
{
if ( doRenderNeedle( gameData, isEditorMode ) )
{
float value = getValue( gameData, isEditorMode );
float minValue = getMinValue( gameData, isEditorMode );
float maxValue = getMaxValue( gameData, isEditorMode );
if ( !getNeedleMayExceedMinimum() )
value = Math.max( minValue, value );
if ( !getNeedleMayExceedMaximum() )
value = Math.min( value, maxValue );
float rot0 = needleRotationForMinValue.getFactoredValue();
float rot = -( ( value - minValue ) / ( maxValue - minValue ) ) * ( needleRotationForMinValue.getFactoredValue() - needleRotationForMaxValue.getFactoredValue() );
needleTexture.setRotation( rot0 + rot );
needleTexture.setVisible( true );
}
else
{
needleTexture.setVisible( false );
}
}
if ( displayValue.getBooleanValue() )
{
valueValue.update( getValueForValueDisplay( gameData, isEditorMode ) );
if ( needsCompleteRedraw || ( clock.c() && valueValue.hasChanged() ) )
{
String string = valueValue.getValueAsString();
FontMetrics metrics = getValueFont().getMetrics();
Rectangle2D bounds = metrics.getStringBounds( string, texture.getTextureCanvas() );
double fw = bounds.getWidth();
if ( valueBackgroundTexture == null )
{
valueString.draw( offsetX - (int)( fw / 2.0 ), offsetY, string, texture );
}
else
{
if ( needsCompleteRedraw )
valueBackgroundTexture.getTexture().clear( valueBackgroundTexture_bak, true, null );
valueString.draw( (int)( -fw / 2.0 ), 0, string, valueBackgroundTexture.getTexture(), valueBackgroundTexture_bak, 0, 0 );
}
}
if ( valueBackgroundTexture != null )
valueBackgroundTexture.setTranslation( valueBackgroundTexPosX, valueBackgroundTexPosY );
}
if ( isEditorMode && ( drawNeeldeMount != null ) )
{
final int centerX = offsetX + getNeedleMountX( width );
final int centerY = offsetY + getNeedleMountY( height );
Texture2DCanvas texCanvas = texture.getTextureCanvas();
Color oldColor = texCanvas.getColor();
texCanvas.setColor( Color.MAGENTA );
Stroke oldStroke = texCanvas.getStroke();
texCanvas.setStroke( new BasicStroke( 3 ) );
texCanvas.drawLine( centerX - 8, centerY - 8, centerX + 8, centerY + 8 );
texCanvas.drawLine( centerX - 8, centerY + 8, centerX + 8, centerY - 8 );
texCanvas.setStroke( oldStroke );
texCanvas.setColor( oldColor );
drawNeeldeMount = null;
}
}
protected void saveMarkersProperties( PropertyWriter writer ) throws IOException
{
writer.writeProperty( displayMarkers, "Display markers?" );
writer.writeProperty( displayMarkerNumbers, "Display marker numbers?" );
writer.writeProperty( markerNumbersInside, "Render marker numbers inside of the markers?" );
writer.writeProperty( markersInnerRadius, "The inner radius of the markers (in background image space)" );
writer.writeProperty( markersLength, "The length of the markers (in background image space)" );
writer.writeProperty( markersOnCircle, "Draw markers on circle, even if the Widget has an aspect ratio unequal to 1.0" );
writer.writeProperty( firstMarkerNumberOffset, "The rotational offset in clockwise degrees for the first marker number." );
writer.writeProperty( lastMarkerNumberOffset, "The rotational offset in clockwise degrees for the last marker number." );
writer.writeProperty( markersBigStep, "Step size of bigger rev markers" );
writer.writeProperty( markersSmallStep, "Step size of smaller rev markers" );
writer.writeProperty( lastMarkerBig, "Whether to force the last marker to be treated as a big one." );
writer.writeProperty( markersColor, "The color used to draw the markers." );
writer.writeProperty( markersFont, "The font used to draw the marker numbers." );
writer.writeProperty( markersFontColor, "The font color used to draw the marker numbers." );
writer.writeProperty( markersFontDropShadowColor, "The font color for the marker numbers drop shadow." );
writer.writeProperty( markerNumbersCentered, "Draw marker numbers at their centers at an equal distance around needle mount?" );
}
protected void saveNeedleProperties( PropertyWriter writer ) throws IOException
{
writer.writeProperty( needleImageName, "The name of the needle image." );
writer.writeProperty( needleMountX, "The x-offset in background image pixels to the needle mount (-1 for center)." );
writer.writeProperty( needleMountY, "The y-offset in background image pixels to the needle mount (-1 for center)." );
writer.writeProperty( needlePivotBottomOffset, "The offset in (unscaled) pixels from the bottom of the image, where the center of the needle's axis is." );
writer.writeProperty( needleRotationForMinValue, "The rotation for the needle image, that it has for min value (in degrees)." );
writer.writeProperty( needleRotationForMaxValue, "The rotation for the needle image, that it has for max value (in degrees)." );
}
protected void saveDigiValueProperties( PropertyWriter writer ) throws IOException
{
writer.writeProperty( displayValue, "Display the digital value?" );
writer.writeProperty( valueBackgroundImageName, "The name of the image to render behind the value number." );
writer.writeProperty( valuePosX, "The x-offset in pixels to the value label." );
writer.writeProperty( valuePosY, "The y-offset in pixels to the value label." );
writer.writeProperty( valueFont, "The font used to draw the value." );
writer.writeProperty( valueFontColor, "The font color used to draw the value." );
}
/**
* {@inheritDoc}
*/
@Override
public void saveProperties( PropertyWriter writer ) throws IOException
{
super.saveProperties( writer );
writer.writeProperty( minValue, "The minimum value accepted for the markers and needle" );
writer.writeProperty( maxValue, "The maximum value accepted for the markers and needle" );
saveMarkersProperties( writer );
saveNeedleProperties( writer );
saveDigiValueProperties( writer );
}
/**
* {@inheritDoc}
*/
@Override
public void loadProperty( PropertyLoader loader )
{
super.loadProperty( loader );
if ( loader.loadProperty( minValue ) );
else if ( loader.loadProperty( maxValue ) );
else if ( loader.loadProperty( displayMarkers ) );
else if ( loader.loadProperty( displayMarkerNumbers ) );
else if ( loader.loadProperty( markerNumbersInside ) );
else if ( loader.loadProperty( markersInnerRadius ) );
else if ( loader.loadProperty( markersLength ) );
else if ( loader.loadProperty( markersOnCircle ) );
else if ( loader.loadProperty( firstMarkerNumberOffset ) );
else if ( loader.loadProperty( lastMarkerNumberOffset ) );
else if ( loader.loadProperty( markersBigStep ) );
else if ( loader.loadProperty( markersSmallStep ) );
else if ( loader.loadProperty( lastMarkerBig ) );
else if ( loader.loadProperty( markersColor ) );
else if ( loader.loadProperty( markersFont ) );
else if ( loader.loadProperty( markersFontColor ) );
else if ( loader.loadProperty( markersFontDropShadowColor ) );
else if ( loader.loadProperty( markerNumbersCentered ) );
else if ( loader.loadProperty( needleImageName ) );
else if ( loader.loadProperty( needleMountX ) );
else if ( loader.loadProperty( needleMountY ) );
else if ( loader.loadProperty( needlePivotBottomOffset ) );
else if ( loader.loadProperty( needleRotationForMinValue ) );
else if ( loader.loadProperty( needleRotationForMaxValue ) );
else if ( loader.loadProperty( displayValue ) );
else if ( loader.loadProperty( valueBackgroundImageName ) );
else if ( loader.loadProperty( valuePosX ) );
else if ( loader.loadProperty( valuePosY ) );
else if ( loader.loadProperty( valueFont ) );
else if ( loader.loadProperty( valueFontColor ) );
}
/**
* Adds the minValue property to the container.
*
* @param propsCont the container to add the properties to
* @param forceAll If <code>true</code>, all properties provided by this {@link Widget} must be added.
* If <code>false</code>, only the properties, that are relevant for the current {@link Widget}'s situation have to be added, some can be ignored.
*/
protected void addMinValuePropertyToContainer( PropertiesContainer propsCont, boolean forceAll )
{
propsCont.addProperty( minValue );
}
/**
* Adds the maxValue property to the container.
*
* @param propsCont the container to add the properties to
* @param forceAll If <code>true</code>, all properties provided by this {@link Widget} must be added.
* If <code>false</code>, only the properties, that are relevant for the current {@link Widget}'s situation have to be added, some can be ignored.
*/
protected void addMaxValuePropertyToContainer( PropertiesContainer propsCont, boolean forceAll )
{
propsCont.addProperty( maxValue );
}
/**
* Collects the widget type specific properties before needle, markers and digi value.
*
* @param propsCont the container to add the properties to
* @param forceAll If <code>true</code>, all properties provided by this {@link Widget} must be added.
* If <code>false</code>, only the properties, that are relevant for the current {@link Widget}'s situation have to be added, some can be ignored.
*
* @return <code>true</code>, if the implementation has added a group, <code>false</code> otherwise.
*/
protected boolean getSpecificPropertiesFirst( PropertiesContainer propsCont, boolean forceAll )
{
propsCont.addGroup( "Misc" );
addMinValuePropertyToContainer( propsCont, forceAll );
addMaxValuePropertyToContainer( propsCont, forceAll );
return ( true );
}
/**
* Collects the properties for the markers.
*
* @param propsCont the container to add the properties to
* @param forceAll If <code>true</code>, all properties provided by this {@link Widget} must be added.
* If <code>false</code>, only the properties, that are relevant for the current {@link Widget}'s situation have to be added, some can be ignored.
*/
protected void getMarkersProperties( PropertiesContainer propsCont, boolean forceAll )
{
propsCont.addGroup( "Markers" );
propsCont.addProperty( displayMarkers );
propsCont.addProperty( displayMarkerNumbers );
propsCont.addProperty( markerNumbersInside );
propsCont.addProperty( markersInnerRadius );
propsCont.addProperty( markersLength );
propsCont.addProperty( markersOnCircle );
propsCont.addProperty( firstMarkerNumberOffset );
propsCont.addProperty( lastMarkerNumberOffset );
propsCont.addProperty( markersBigStep );
propsCont.addProperty( markersSmallStep );
propsCont.addProperty( lastMarkerBig );
propsCont.addProperty( markersColor );
propsCont.addProperty( markersFont );
propsCont.addProperty( markersFontColor );
propsCont.addProperty( markersFontDropShadowColor );
propsCont.addProperty( markerNumbersCentered );
}
/**
* Collects the properties for the needle.
*
* @param propsCont the container to add the properties to
* @param forceAll If <code>true</code>, all properties provided by this {@link Widget} must be added.
* If <code>false</code>, only the properties, that are relevant for the current {@link Widget}'s situation have to be added, some can be ignored.
*/
protected void getNeedleProperties( PropertiesContainer propsCont, boolean forceAll )
{
propsCont.addGroup( "Needle" );
propsCont.addProperty( needleImageName );
propsCont.addProperty( needleMountX );
propsCont.addProperty( needleMountY );
propsCont.addProperty( needlePivotBottomOffset );
propsCont.addProperty( needleRotationForMinValue );
propsCont.addProperty( needleRotationForMaxValue );
}
/**
* Gets the display name of the properties group for the digital value in the editor.
*
* @return the display name of the properties group for the digital value in the editor.
*/
protected String getDigiValuePropertiesGroupName()
{
return ( "Digital Value" );
}
/**
* Collects the properties for the digital value.
*
* @param propsCont the container to add the properties to
* @param forceAll If <code>true</code>, all properties provided by this {@link Widget} must be added.
* If <code>false</code>, only the properties, that are relevant for the current {@link Widget}'s situation have to be added, some can be ignored.
*/
protected void getDigiValueProperties( PropertiesContainer propsCont, boolean forceAll )
{
propsCont.addGroup( getDigiValuePropertiesGroupName() );
propsCont.addProperty( displayValue );
propsCont.addProperty( valueBackgroundImageName );
propsCont.addProperty( valuePosX );
propsCont.addProperty( valuePosY );
propsCont.addProperty( valueFont );
propsCont.addProperty( valueFontColor );
}
/**
* {@inheritDoc}
*/
@Override
public void getProperties( PropertiesContainer propsCont, boolean forceAll )
{
super.getProperties( propsCont, forceAll );
getSpecificPropertiesFirst( propsCont, forceAll );
getMarkersProperties( propsCont, forceAll );
getNeedleProperties( propsCont, forceAll );
getDigiValueProperties( propsCont, forceAll );
}
/**
* This method is called as the last item in the constructor.
*/
protected void initParentProperties()
{
}
/**
* {@inheritDoc}
*/
@Override
public void prepareForMenuItem()
{
super.prepareForMenuItem();
markersFont.setFont( "Dialog", Font.PLAIN, 5, false, true );
valueFont.setFont( "Dialog", Font.PLAIN, 5, false, true );
}
/**
* Creates a new {@link NeedleMeterWidget}.
*
* @param widgetSet the {@link WidgetSet} this {@link Widget} belongs to
* @param widgetPackage the package in the editor
* @param width negative numbers for (screen_width - width)
* @param widthPercent width parameter treated as percents
* @param height negative numbers for (screen_height - height)
* @param heightPercent height parameter treated as percents
*/
public NeedleMeterWidget( WidgetSet widgetSet, WidgetPackage widgetPackage, float width, boolean widthPercent, float height, boolean heightPercent )
{
super( widgetSet, widgetPackage, width, widthPercent, height, heightPercent );
initParentProperties();
}
/**
* Creates a new {@link NeedleMeterWidget}.
*
* @param widgetSet the {@link WidgetSet} this {@link Widget} belongs to
* @param widgetPackage the package in the editor
* @param width negative numbers for (screen_width - width)
* @param height negative numbers for (screen_height - height)
*/
public NeedleMeterWidget( WidgetSet widgetSet, WidgetPackage widgetPackage, float width, float height )
{
this( widgetSet, widgetPackage, width, true, height, true );
}
}