Hi all,
sorry, I am not sure if this is the correct forum for the question I have.
We are developing synthetic radar displays for ATC purposes. So far we have been using Swing for that. For the last year I am trying to make a push towards JavaFX (especially now with Java 8 on stage).
I came up with several example applications that try to imitate existing systems. So far so good, but the case I am trying right now is basically a canvas with some map-like data and some text. In fact we have quite a bit of text (e.g. radar target labels).
I wrote a little JavaFX program that renders some targets with text on the canvas. The canvas also supports mouse wheel zooming and panning. This is working ok with e.g. 50 targets but not with 500 targets (and we need to display even more then that). The performance is completely gone. It is getting a lot better when I omit the label text rendering.
For comparison purposes I wrote the same thing using Swing and it performs very well even with much more targets.
Why is it that JavaFX canvas is so slow in rendering text? Is there anything I can do?
Please try it out of yourself. I tried to make the two examples as small as possible. Here is the JavaFX example:
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.ArcType;
import javafx.stage.Stage;
public final class JavaFXCanvasDemo extends Application
{
private static final Rectangle2D MAP_RECT = new Rectangle2D( -50.0, -50.0, 100.0, 100.0 );
private static final int OBJECT_COUNT = 500;
private static boolean PAINT_LABELS = true; // Set this to 'false' for real speedup but then ... no labels :-(
private Map< String, Point2D > points_;
private Canvas canvas_;
private double xOffset_;
private double yOffset_;
private double scale_ = 1.0;
private double dragStartX_;
private double dragStartY_;
private Color backgroundColor_;
private Color foregroundColor_;
@Override
public void init() throws Exception
{
backgroundColor_ = Color.grayRgb( 0xc8 );
foregroundColor_ = Color.rgb( 0x04, 0x87, 0xf9 );
points_ = new HashMap<>();
Random random = new Random();
for ( int i = 0; i < OBJECT_COUNT; ++i )
{
double x = random.nextDouble() * MAP_RECT.getWidth() + MAP_RECT.getMinX();
double y = random.nextDouble() * MAP_RECT.getHeight() + MAP_RECT.getMinY();
points_.put( String.format( "Point%04d", i ), new Point2D( x, y ) );
}
}
@Override
public void start( Stage stage ) throws Exception
{
Group root = new Group();
canvas_ = new Canvas( 800, 800 );
root.getChildren().add( canvas_ );
stage.setTitle( "JavaFX Canvas Demo" );
stage.setScene( new Scene( root, backgroundColor_ ) );
stage.show();
canvas_.widthProperty().bind( stage.widthProperty() );
canvas_.heightProperty().bind( stage.heightProperty() );
ChangeListener< Number > dimensionChangeListener = new ChangeListener< Number >()
{
@Override
public void changed( ObservableValue< ? extends Number > arg0, Number arg1, Number arg2 )
{
drawObjects( canvas_.getGraphicsContext2D() );
}
};
canvas_.widthProperty().addListener( dimensionChangeListener );
canvas_.heightProperty().addListener( dimensionChangeListener );
canvas_.setOnScroll( new EventHandler< ScrollEvent >()
{
@Override
public void handle( ScrollEvent ev )
{
Point2D focalPoint = canvas2Physical( ev.getX(), ev.getY() );
if ( ev.getDeltaY() > 0.0 )
{
scale_ *= 1.1;
}
else
{
scale_ *= 0.9;
}
Point2D resetPoint = physical2Canvas( focalPoint.getX(), focalPoint.getY() );
xOffset_ -= ( resetPoint.getX() - ev.getX() );
yOffset_ -= ( resetPoint.getY() - ev.getY() );
drawObjects( canvas_.getGraphicsContext2D() );
}
} );
canvas_.setOnMousePressed( new EventHandler< MouseEvent >()
{
@Override
public void handle( MouseEvent ev )
{
dragStartX_ = ev.getX();
dragStartY_ = ev.getY();
}
} );
canvas_.setOnMouseDragged( new EventHandler< MouseEvent >()
{
@Override
public void handle( MouseEvent ev )
{
xOffset_ += ev.getX() - dragStartX_;
dragStartX_ = ev.getX();
yOffset_ += ev.getY() - dragStartY_;
dragStartY_ = ev.getY();
drawObjects( canvas_.getGraphicsContext2D() );
}
} );
fullScaleAndCenter( canvas_ );
drawObjects( canvas_.getGraphicsContext2D() );
}
private void drawObjects( GraphicsContext gc )
{
gc.clearRect( 0, 0, gc.getCanvas().getWidth(), gc.getCanvas().getHeight() );
gc.setStroke( foregroundColor_ );
for ( String id : points_.keySet() )
{
Point2D point = points_.get( id );
Point2D canvasPos = physical2Canvas( point.getX(), point.getY() );
gc.strokeArc( canvasPos.getX(), canvasPos.getY(), 10.0, 10.0, 0.0, 360.0, ArcType.OPEN );
gc.strokeLine( canvasPos.getX(), canvasPos.getY() + 5.0, canvasPos.getX() + 10.0, canvasPos.getY() + 5.0 );
gc.strokeLine( canvasPos.getX() + 5.0, canvasPos.getY(), canvasPos.getX() + 5.0, canvasPos.getY() + 10.0 );
if ( PAINT_LABELS )
{
gc.strokeText( id, canvasPos.getX() + 10.0, canvasPos.getY() - 10.0 );
}
}
}
private void fullScaleAndCenter( Canvas canvas )
{
double width = MAP_RECT.getWidth();
double height = MAP_RECT.getHeight();
double scaleX = canvas.getWidth() / width;
double scaleY = canvas.getHeight() / height;
scale_ = Math.min( scaleX, scaleY );
xOffset_ = canvas.getWidth() / 2.0;
yOffset_ = canvas.getHeight() / 2.0;
}
private Point2D physical2Canvas( double x, double y )
{
return new Point2D( ( x * scale_ + xOffset_ ), ( y * ( -scale_ ) + yOffset_ ) );
}
private Point2D canvas2Physical( double i, double j )
{
return new Point2D( ( i - xOffset_ ) / scale_, ( j - yOffset_ ) / ( -scale_ ) );
}
public static void main( String[] args )
{
launch( args );
}
}
And here is the same thing using Swing:
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.event.MouseInputAdapter;
@SuppressWarnings( "serial" )
public final class SwingPanelDemo extends JPanel
{
private static final Rectangle2D MAP_RECT =
new Rectangle2D.Double( -50.0, -50.0, 100.0, 100.0 );
private static final int OBJECT_COUNT = 500;
private final Map< String, Point2D > points_;
private double xOffset_;
private double yOffset_;
private double scale_ = 1.0;
private double dragStartX_;
private double dragStartY_;
private final Color backgroundColor_;
private final Color foregroundColor_;
private SwingPanelDemo()
{
super( null );
backgroundColor_ = new Color( 0xc8c8c8 );
foregroundColor_ = new Color( 0x0487f9 );
points_ = new HashMap<>();
Random random = new Random();
for ( int i = 0; i < OBJECT_COUNT; ++i )
{
double x = random.nextDouble() * MAP_RECT.getWidth() + MAP_RECT.getMinX();
double y = random.nextDouble() * MAP_RECT.getHeight() + MAP_RECT.getMinY();
points_.put( String.format( "Point%04d", i ), new Point2D.Double( x, y ) );
}
MouseInputAdapter mouseListener = new MouseInputAdapter()
{
@Override
public void mousePressed( MouseEvent ev )
{
dragStartX_ = ev.getX();
dragStartY_ = ev.getY();
}
@Override
public void mouseDragged( MouseEvent ev )
{
xOffset_ += ev.getX() - dragStartX_;
dragStartX_ = ev.getX();
yOffset_ += ev.getY() - dragStartY_;
dragStartY_ = ev.getY();
repaint();
}
@Override
public void mouseWheelMoved( MouseWheelEvent ev )
{
Point2D focalPoint = canvas2Physical( ev.getX(), ev.getY() );
if ( ev.getWheelRotation() < 0 )
{
scale_ *= 1.1;
}
else
{
scale_ *= 0.9;
}
Point2D resetPoint = physical2Canvas( focalPoint.getX(), focalPoint.getY() );
xOffset_ -= ( resetPoint.getX() - ev.getX() );
yOffset_ -= ( resetPoint.getY() - ev.getY() );
repaint();
}
};
addMouseListener( mouseListener );
addMouseMotionListener( mouseListener );
addMouseWheelListener( mouseListener );
}
@Override
protected void paintComponent( Graphics g )
{
Graphics2D g2d = (Graphics2D) g;
g2d.setBackground( backgroundColor_ );
g2d.clearRect( 0, 0, getWidth(), getHeight() );
g2d.setColor( foregroundColor_ );
for ( String id : points_.keySet() )
{
Point2D point = points_.get( id );
Point2D canvasPos = physical2Canvas( point.getX(), point.getY() );
g2d.drawArc( (int) canvasPos.getX(), (int) canvasPos.getY(), 10, 10, 0, 360 );
g2d.drawLine( (int) canvasPos.getX(), (int) canvasPos.getY() + 5, (int) canvasPos.getX() + 10, (int) canvasPos.getY() + 5 );
g2d.drawLine( (int) canvasPos.getX() + 5, (int) canvasPos.getY(), (int) canvasPos.getX() + 5, (int) canvasPos.getY() + 10 );
g2d.drawString( id, (int) canvasPos.getX() + 10, (int) canvasPos.getY() - 10 );
}
}
private void fullScaleAndCenter()
{
double width = MAP_RECT.getWidth();
double height = MAP_RECT.getHeight();
double scaleX = getWidth() / width;
double scaleY = getHeight() / height;
scale_ = Math.min( scaleX, scaleY );
xOffset_ = getWidth() / 2.0;
yOffset_ = getHeight() / 2.0;
}
private Point2D physical2Canvas( double x, double y )
{
return new Point2D.Double( ( x * scale_ + xOffset_ ), ( y * ( -scale_ ) + yOffset_ ) );
}
private Point2D canvas2Physical( double i, double j )
{
return new Point2D.Double( ( i - xOffset_ ) / scale_, ( j - yOffset_ ) / ( -scale_ ) );
}
public static void main( String[] args )
{
SwingPanelDemo panel = new SwingPanelDemo();
JFrame frame = new JFrame( "Swing Panel Demo" );
frame.setContentPane( panel );
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
frame.setSize( 800, 800 );
frame.setVisible( true );
panel.fullScaleAndCenter();
panel.repaint();
}
}
Thanks for your support.
Greeting,
Ralph