/** * 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.revneedlemeter; import java.awt.Font; import java.awt.FontMetrics; import java.awt.geom.Rectangle2D; import java.io.IOException; import net.ctdp.rfdynhud.gamedata.LiveGameData; import net.ctdp.rfdynhud.gamedata.VehicleScoringInfo; import net.ctdp.rfdynhud.properties.BooleanProperty; import net.ctdp.rfdynhud.properties.ColorProperty; import net.ctdp.rfdynhud.properties.DelayProperty; import net.ctdp.rfdynhud.properties.FontProperty; import net.ctdp.rfdynhud.properties.ImageProperty; import net.ctdp.rfdynhud.properties.IntProperty; 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.SubTextureCollector; import net.ctdp.rfdynhud.util.PropertyWriter; import net.ctdp.rfdynhud.valuemanagers.Clock; import net.ctdp.rfdynhud.values.FloatValue; import net.ctdp.rfdynhud.values.IntValue; import net.ctdp.rfdynhud.widgets.base.needlemeter.NeedleMeterWidget; 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 AbstractRevNeedleMeterWidget} displays rev/RPM information. * * @author Marvin Froehlich (CTDP) */ public abstract class AbstractRevNeedleMeterWidget extends NeedleMeterWidget { public static final int PEAK_NEEDLE_LOCAL_Z_INDEX = NEEDLE_LOCAL_Z_INDEX - 1; public static final String DEFAULT_GEAR_FONT_NAME = "GearFont"; private final BooleanProperty hideWhenViewingOtherCar = new BooleanProperty( "hideWhenViewingOtherCar", "hideWhenOtherCar", false ); @Override protected int getMarkersBigStepLowerLimit() { return ( 300 ); } @Override protected int getMarkersSmallStepLowerLimit() { return ( 20 ); } protected final BooleanProperty useMaxRevLimit = new BooleanProperty( "useMaxRevLimit", true ); protected final BooleanProperty displayGear = new BooleanProperty( "displayGear", "displayGear", true ); protected final ImageProperty gearBackgroundImageName = new ImageProperty( "gearBackgroundImageName", "bgImageName", "", false, true ); private TransformableTexture gearBackgroundTexture = null; private TextureImage2D gearBackgroundTexture_bak = null; protected final IntProperty gearPosX = new IntProperty( "gearPosX", "posX", 354 ); protected final IntProperty gearPosY = new IntProperty( "gearPosY", "posY", 512 ); private int gearBackgroundTexPosX, gearBackgroundTexPosY; protected final FontProperty gearFont = new FontProperty( "gearFont", "font", DEFAULT_GEAR_FONT_NAME ); protected final ColorProperty gearFontColor = new ColorProperty( "gearFontColor", "fontColor", "#1A261C" ); protected String getInitialPeakNeedleImage() { return ( "standard/rev_meter_needle.png" ); } protected final ImageProperty peakNeedleImageName = new ImageProperty( "peakNeedleImageName", "imageName", getInitialPeakNeedleImage(), false, true ); protected final IntProperty peakNeedlePivotBottomOffset = new IntProperty( "peakNeedlePivotBottomOffset", "pivotBottomOffset", 60 ); private TransformableTexture peakNeedleTexture = null; protected final DelayProperty peakNeedleCooldown = new DelayProperty( "peakNeedleCooldown", "cooldown", DelayProperty.DisplayUnits.MILLISECONDS, 1000, 0, 5000, false ); protected final DelayProperty peakNeedleWaitTime = new DelayProperty( "peakNeedleWaitTime", "wait", DelayProperty.DisplayUnits.MILLISECONDS, 1000, 0, 5000, false ); protected final DelayProperty peakNeedleDownshiftIgnoreTime = new DelayProperty( "peakNeedleDownshiftIgnoreTime", "downshiftIgnoreTime", DelayProperty.DisplayUnits.MILLISECONDS, 1500, 0, 5000, false ); private long nextPeakRecordTime = -1L; private long lastPeakRecordTime = -1L; private DrawnString gearString = null; private final FloatValue maxRPMCheck = new FloatValue(); private final IntValue gear = new IntValue(); private float peakRPM = 0f; private short lastGear = 0; @Override protected void onBackgroundChanged( boolean imageChanged, float deltaScaleX, float deltaScaleY ) { super.onBackgroundChanged( imageChanged, deltaScaleX, deltaScaleY ); // TODO: Don't set to null! gearBackgroundTexture = null; gearBackgroundTexture_bak = null; if ( deltaScaleX > 0f ) { gearPosX.setIntValue( Math.round( gearPosX.getIntValue() * deltaScaleX ) ); gearPosY.setIntValue( Math.round( gearPosY.getIntValue() * deltaScaleY ) ); } } /** * {@inheritDoc} */ @Override public String getDefaultNamedFontValue( String name ) { String result = super.getDefaultNamedFontValue( name ); if ( result != null ) return ( result ); if ( name.equals( DEFAULT_GEAR_FONT_NAME ) ) return ( "Monospaced" + FontUtils.SEPARATOR + "BOLD" + FontUtils.SEPARATOR + "26va" ); return ( null ); } /** * {@inheritDoc} */ @Override public int getNeededData() { return ( Widget.NEEDED_DATA_TELEMETRY/* | Widget.NEEDED_DATA_SETUP*/ ); } @Override protected float getMinDataValue( LiveGameData gameData, boolean isEditorModes ) { return ( 0 ); } @Override protected float getMaxDataValue( LiveGameData gameData, boolean isEditorMode ) { /* if ( useMaxRevLimit.getBooleanValue() ) return ( gameData.getPhysics().getEngine().getRevLimitRange().getMaxValue() ); return ( gameData.getTelemetryData().getEngineMaxRPM() ); */ if ( useMaxRevLimit.getBooleanValue() ) return ( gameData.getPhysics().getEngine().getMaxRPM( gameData.getPhysics().getEngine().getRevLimitRange().getMaxValue() ) ); return ( gameData.getPhysics().getEngine().getMaxRPM( gameData.getSetup().getEngine().getRevLimit() ) ); } @Override protected float getValue( LiveGameData gameData, boolean isEditorMode ) { return ( gameData.getScoringInfo().getViewedVehicleScoringInfo().getEngineRPM() ); } @Override protected int getValueForValueDisplay( LiveGameData gameData, boolean isEditorMode ) { VehicleScoringInfo vsi = gameData.getScoringInfo().getViewedVehicleScoringInfo(); if ( vsi.isPlayer() ) return ( Math.round( gameData.getTelemetryData().getScalarVelocity() ) ); return ( Math.round( vsi.getScalarVelocity() ) ); } @Override protected String getMarkerLabelForValue( LiveGameData gameData, boolean isEditorMode, float value ) { return ( String.valueOf( Math.round( value / 1000 ) ) ); } public void setPeakNeedleImageName( String image ) { this.peakNeedleImageName.setImageName( image ); } public final String getPeakNeedleImageName() { return ( peakNeedleImageName.getImageName() ); } private boolean loadPeakNeedleTexture( boolean isEditorMode ) { if ( peakNeedleImageName.isNoImage() ) { peakNeedleTexture = null; return ( false ); } try { ImageTemplate it = peakNeedleImageName.getImage(); if ( it == null ) { peakNeedleTexture = null; return ( false ); } float scale = getBackground().getScaleX(); int w = Math.round( it.getBaseWidth() * scale ); int h = Math.round( it.getBaseHeight() * scale ); peakNeedleTexture = it.getScaledTransformableTexture( w, h, peakNeedleTexture, isEditorMode ); peakNeedleTexture.setLocalZIndex( PEAK_NEEDLE_LOCAL_Z_INDEX ); } catch ( Throwable t ) { log( t ); return ( false ); } return ( true ); } private boolean loadGearBackgroundTexture( boolean isEditorMode ) { if ( !displayGear.getBooleanValue() ) { gearBackgroundTexture = null; gearBackgroundTexture_bak = null; return ( false ); } try { ImageTemplate it = gearBackgroundImageName.getImage(); if ( it == null ) { gearBackgroundTexture = null; gearBackgroundTexture_bak = null; return ( false ); } float scale = getBackground().getScaleX(); int w = Math.round( it.getBaseWidth() * scale ); int h = Math.round( it.getBaseHeight() * scale ); boolean[] changeInfo = new boolean[ 2 ]; gearBackgroundTexture = it.getScaledTransformableTexture( w, h, gearBackgroundTexture, isEditorMode, changeInfo ); gearBackgroundTexture.setDynamic( true ); if ( changeInfo[1] ) // redrawn { gearBackgroundTexture_bak = TextureImage2D.getOrCreateDrawTexture( w, h, it.hasAlpha(), gearBackgroundTexture_bak, isEditorMode ); gearBackgroundTexture_bak.clear( gearBackgroundTexture.getTexture(), true, null ); } } catch ( Throwable t ) { log( t ); return ( false ); } return ( true ); } @Override protected void initSubTextures( LiveGameData gameData, boolean isEditorMode, int widgetInnerWidth, int widgetInnerHeight, SubTextureCollector collector ) { super.initSubTextures( gameData, isEditorMode, widgetInnerWidth, widgetInnerHeight, collector ); if ( loadPeakNeedleTexture( isEditorMode ) ) collector.add( peakNeedleTexture ); if ( loadGearBackgroundTexture( isEditorMode ) ) collector.add( gearBackgroundTexture ); } /** * {@inheritDoc} */ @Override public void onCockpitEntered( LiveGameData gameData, boolean isEditorMode ) { super.onCockpitEntered( gameData, isEditorMode ); lastGear = 0; peakRPM = 0f; maxRPMCheck.reset(); gear.reset(); } /** * {@inheritDoc} */ @Override public void onNeededDataComplete( LiveGameData gameData, boolean isEditorMode ) { super.onNeededDataComplete( gameData, isEditorMode ); lastGear = 0; peakRPM = 0f; maxRPMCheck.reset(); gear.reset(); } /** * {@inheritDoc} */ @Override protected Boolean onVehicleControlChanged( VehicleScoringInfo viewedVSI, LiveGameData gameData, boolean isEditorMode ) { super.onVehicleControlChanged( viewedVSI, gameData, isEditorMode ); return ( viewedVSI.isPlayer() || !hideWhenViewingOtherCar.getBooleanValue() ); } /** * {@inheritDoc} */ @Override protected void initialize( LiveGameData gameData, boolean isEditorMode, DrawnStringFactory dsf, TextureImage2D texture, int width, int height ) { super.initialize( gameData, isEditorMode, dsf, texture, width, height ); final Texture2DCanvas texCanvas = texture.getTextureCanvas(); FontMetrics metrics = null; Rectangle2D bounds = null; double fw = 0, fh = 0; double fd = 0; int fx = 0, fy = 0; if ( displayGear.getBooleanValue() ) { metrics = gearFont.getMetrics(); bounds = metrics.getStringBounds( "X", texCanvas ); fw = bounds.getWidth(); fd = metrics.getDescent(); fh = metrics.getAscent() - fd; if ( gearBackgroundTexture == null ) { fx = Math.round( gearPosX.getIntValue() * getBackground().getScaleX() ); fy = Math.round( gearPosY.getIntValue() * getBackground().getScaleY() ); } else { gearBackgroundTexPosX = Math.round( gearPosX.getIntValue() * getBackground().getScaleX() - gearBackgroundTexture.getWidth() / 2.0f ); gearBackgroundTexPosY = Math.round( gearPosY.getIntValue() * getBackground().getScaleY() - gearBackgroundTexture.getHeight() / 2.0f ); fx = gearBackgroundTexture.getWidth() / 2; fy = gearBackgroundTexture.getHeight() / 2; } } gearString = dsf.newDrawnStringIf( displayGear.getBooleanValue(), "gearString", fx - (int)( fw / 2.0 ), fy - (int)( fd + fh / 2.0 ), Alignment.LEFT, false, gearFont.getFont(), gearFont.isAntiAliased(), gearFontColor.getColor() ); int mountX = getNeedleMountX( width ); int mountY = getNeedleMountY( height ); if ( peakNeedleTexture != null ) { peakNeedleTexture.setTranslation( mountX - peakNeedleTexture.getWidth() / 2, mountY - peakNeedleTexture.getHeight() + peakNeedlePivotBottomOffset.getIntValue() * getBackground().getScaleX() ); peakNeedleTexture.setRotationCenter( peakNeedleTexture.getWidth() / 2, (int)( peakNeedleTexture.getHeight() - peakNeedlePivotBottomOffset.getIntValue() * getBackground().getScaleY() ) ); } } /** * {@inheritDoc} */ @Override protected boolean checkForChanges( LiveGameData gameData, boolean isEditorMode, TextureImage2D texture, int width, int height ) { maxRPMCheck.update( gameData.getTelemetryData().getEngineMaxRPM() ); if ( maxRPMCheck.hasChanged() ) return ( true ); return ( false ); } @Override protected boolean doRenderNeedle( LiveGameData gameData, boolean isEditorMode ) { VehicleScoringInfo vsi = gameData.getScoringInfo().getViewedVehicleScoringInfo(); return ( vsi.isPlayer() ); } @Override protected void drawWidget( Clock clock, boolean needsCompleteRedraw, LiveGameData gameData, boolean isEditorMode, TextureImage2D texture, int offsetX, int offsetY, int width, int height ) { super.drawWidget( clock, needsCompleteRedraw, gameData, isEditorMode, texture, offsetX, offsetY, width, height ); VehicleScoringInfo vsi = gameData.getScoringInfo().getViewedVehicleScoringInfo(); if ( displayGear.getBooleanValue() ) { gear.update( vsi.getCurrentGear() ); if ( needsCompleteRedraw || gear.hasChanged() ) { String string; if ( vsi.isPlayer() ) string = gear.getValue() == -1 ? "R" : gear.getValue() == 0 ? "N" : String.valueOf( gear ); else string = ""; if ( gearBackgroundTexture == null ) { gearString.draw( offsetX, offsetY, string, texture ); } else { if ( needsCompleteRedraw ) gearBackgroundTexture.getTexture().clear( gearBackgroundTexture_bak, true, null ); gearString.draw( 0, 0, string, gearBackgroundTexture.getTexture(), gearBackgroundTexture_bak, 0, 0 ); } } } if ( gearBackgroundTexture != null ) gearBackgroundTexture.setTranslation( gearBackgroundTexPosX, gearBackgroundTexPosY ); if ( peakNeedleTexture != null ) { if ( doRenderNeedle( gameData, isEditorMode ) ) { long sessionNanos = gameData.getScoringInfo().getSessionNanos(); float rpmRange = ( getMaxValue( gameData, isEditorMode ) - getMinValue( gameData, isEditorMode ) ); float value; if ( isEditorMode ) { value = getMaxValue( gameData, isEditorMode ); } else { value = peakRPM; float rpm2 = Math.max( 0f, vsi.getEngineRPM() ); if ( rpm2 < peakRPM - 5f ) { if ( sessionNanos > lastPeakRecordTime + peakNeedleWaitTime.getDelayNanos() ) { float cooldown = (float)( ( sessionNanos - lastPeakRecordTime - peakNeedleWaitTime.getDelayNanos() ) / 1000000000.0 ) / ( peakNeedleCooldown.getDelaySeconds() * ( peakRPM / rpmRange ) ); if ( cooldown > 1.0f ) cooldown = 1.0f; value = Math.max( rpm2, peakRPM - ( peakRPM * cooldown ) ); } } if ( gear.getValue() < lastGear ) { nextPeakRecordTime = sessionNanos + peakNeedleDownshiftIgnoreTime.getDelayNanos(); } lastGear = (short)gear.getValue(); if ( sessionNanos > nextPeakRecordTime ) { if ( ( rpm2 >= peakRPM ) || ( rpm2 >= value ) ) { peakRPM = rpm2; } if ( Math.abs( rpm2 - peakRPM ) < 5f ) { lastPeakRecordTime = sessionNanos; } } } 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 ) / rpmRange ) * ( needleRotationForMinValue.getFactoredValue() - needleRotationForMaxValue.getFactoredValue() ); peakNeedleTexture.setRotation( rot0 + rot ); peakNeedleTexture.setVisible( true ); } else { peakNeedleTexture.setVisible( false ); } } } @Override protected void initParentProperties() { super.initParentProperties(); markersBigStep.setIntValue( 1000 ); markersSmallStep.setIntValue( 200 ); } @Override protected void saveMarkersProperties( PropertyWriter writer ) throws IOException { super.saveMarkersProperties( writer ); writer.writeProperty( useMaxRevLimit, "Whether to use maximum possible (by setup) rev limit" ); } @Override protected void saveNeedleProperties( PropertyWriter writer ) throws IOException { super.saveNeedleProperties( writer ); writer.writeProperty( peakNeedleImageName, "The name of the peak needle image." ); writer.writeProperty( peakNeedlePivotBottomOffset, "The offset in (unscaled) pixels from the bottom of the image, where the center of the peak needle's axis is." ); writer.writeProperty( peakNeedleWaitTime, "The time in milliseconds to let the peak needle stay at the peak value." ); writer.writeProperty( peakNeedleCooldown, "The time in milliseconds, that the peak needle takes to go down from max RPM to zero." ); writer.writeProperty( peakNeedleDownshiftIgnoreTime, "The time in milliseconds to ignore current revs after a downshift." ); } protected void saveGearProperties( PropertyWriter writer ) throws IOException { writer.writeProperty( displayGear, "Display the gear?" ); writer.writeProperty( gearBackgroundImageName, "The name of the image to render behind the gear number." ); writer.writeProperty( gearPosX, "The x-offset in pixels to the gear label." ); writer.writeProperty( gearPosY, "The y-offset in pixels to the gear label." ); writer.writeProperty( gearFont, "The font used to draw the gear." ); writer.writeProperty( gearFontColor, "The font color used to draw the gear." ); } /** * {@inheritDoc} */ @Override public void saveProperties( PropertyWriter writer ) throws IOException { super.saveProperties( writer ); writer.writeProperty( hideWhenViewingOtherCar, "Hide the Widget when another car is being observed?" ); saveGearProperties( writer ); } /** * {@inheritDoc} */ @Override public void loadProperty( PropertyLoader loader ) { super.loadProperty( loader ); if ( loader.loadProperty( hideWhenViewingOtherCar ) ); else if ( loader.loadProperty( useMaxRevLimit ) ); else if ( loader.loadProperty( peakNeedleImageName ) ); else if ( loader.loadProperty( peakNeedlePivotBottomOffset ) ); else if ( loader.loadProperty( peakNeedleWaitTime ) ); else if ( loader.loadProperty( peakNeedleCooldown ) ); else if ( loader.loadProperty( peakNeedleDownshiftIgnoreTime ) ); else if ( loader.loadProperty( displayGear ) ); else if ( loader.loadProperty( gearBackgroundImageName ) ); else if ( loader.loadProperty( gearPosX ) ); else if ( loader.loadProperty( gearPosY ) ); else if ( loader.loadProperty( gearFont ) ); else if ( loader.loadProperty( gearFontColor ) ); } /** * {@inheritDoc} */ @Override protected void addVisibilityPropertiesToContainer( PropertiesContainer propsCont, boolean forceAll ) { super.addVisibilityPropertiesToContainer( propsCont, forceAll ); if ( getMasterWidget() == null ) { propsCont.addProperty( hideWhenViewingOtherCar ); } } /** * {@inheritDoc} */ @Override protected void addMaxValuePropertyToContainer( PropertiesContainer propsCont, boolean forceAll ) { // We don't need this here! } /** * {@inheritDoc} */ @Override protected void getMarkersProperties( PropertiesContainer propsCont, boolean forceAll ) { super.getMarkersProperties( propsCont, forceAll ); propsCont.addProperty( useMaxRevLimit ); } /** * {@inheritDoc} */ @Override protected void getNeedleProperties( PropertiesContainer propsCont, boolean forceAll ) { super.getNeedleProperties( propsCont, forceAll ); propsCont.addGroup( "Peak Needle" ); propsCont.addProperty( peakNeedleImageName ); propsCont.addProperty( peakNeedlePivotBottomOffset ); propsCont.addProperty( peakNeedleWaitTime ); propsCont.addProperty( peakNeedleCooldown ); propsCont.addProperty( peakNeedleDownshiftIgnoreTime ); } /** * Collects the properties for the gear. * * @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 getGearProperties( PropertiesContainer propsCont, boolean forceAll ) { propsCont.addGroup( "Gear" ); propsCont.addProperty( displayGear ); propsCont.addProperty( gearBackgroundImageName ); propsCont.addProperty( gearPosX ); propsCont.addProperty( gearPosY ); propsCont.addProperty( gearFont ); propsCont.addProperty( gearFontColor ); } /** * {@inheritDoc} */ @Override protected String getDigiValuePropertiesGroupName() { return ( "Velocity" ); } /** * {@inheritDoc} */ @Override public void getProperties( PropertiesContainer propsCont, boolean forceAll ) { super.getProperties( propsCont, forceAll ); getGearProperties( propsCont, forceAll ); } /** * {@inheritDoc} */ @Override public void prepareForMenuItem() { super.prepareForMenuItem(); gearFont.setFont( "Dialog", Font.PLAIN, 8, false, true ); } public AbstractRevNeedleMeterWidget( WidgetSet widgetSet, WidgetPackage widgetPackage, float width, float height ) { super( widgetSet, widgetPackage, width, height ); getBorderProperty().setBorder( "" ); } }