3D Scatter Chart JavaFX: How to show legend and measures near axis - java

Looking this post, I've tried to implement in javaFX, with many difficulties, a Scatter Chart 3D where the grid is my x,y and z axis and the spheres are my points.
How Can I put a legend, axis labels and the range numbers along the axis? I can use only javaFX without external library.
I'm desperate.. I'm trying for days..without results
Please:help me
Thanks.
Code
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
public class GraphingData extends Application {
private static Random rnd = new Random();
// size of graph
int graphSize = 400;
// variables for mouse interaction
private double mousePosX, mousePosY;
private double mouseOldX, mouseOldY;
private final Rotate rotateX = new Rotate(150, Rotate.X_AXIS);
private final Rotate rotateY = new Rotate(120, Rotate.Y_AXIS);
#Override
public void start(Stage primaryStage) {
// create axis walls
Group grid = createGrid(graphSize);
// initial cube rotation
grid.getTransforms().addAll(rotateX, rotateY);
// add objects to scene
StackPane root = new StackPane();
root.getChildren().add(grid);
root.setStyle( "-fx-border-color: red;");
// create bars
double gridSizeHalf = graphSize / 2;
double size = 30;
//Drawing a Sphere
Sphere sphere = new Sphere();
//Setting the properties of the Sphere
sphere.setRadius(10.0);
sphere.setTranslateX(-50);
sphere.setTranslateY(-50);
//Preparing the phong material of type specular color
PhongMaterial material6 = new PhongMaterial();
//setting the specular color map to the material
material6.setDiffuseColor(Color.GREEN);
sphere.setMaterial(material6);
grid.getChildren().addAll(sphere);
// scene
Scene scene = new Scene(root, 1600, 900, true, SceneAntialiasing.BALANCED);
scene.setCamera(new PerspectiveCamera());
scene.setOnMousePressed(me -> {
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
});
scene.setOnMouseDragged(me -> {
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
rotateX.setAngle(rotateX.getAngle() - (mousePosY - mouseOldY));
rotateY.setAngle(rotateY.getAngle() + (mousePosX - mouseOldX));
mouseOldX = mousePosX;
mouseOldY = mousePosY;
});
makeZoomable(root);
primaryStage.setResizable(false);
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* Axis wall
*/
public static class Axis extends Pane {
Rectangle wall;
public Axis(double size) {
// wall
// first the wall, then the lines => overlapping of lines over walls
// works
wall = new Rectangle(size, size);
getChildren().add(wall);
// grid
double zTranslate = 0;
double lineWidth = 1.0;
Color gridColor = Color.RED;
for (int y = 0; y <= size; y += size / 10) {
Line line = new Line(0, 0, size, 0);
line.setStroke(gridColor);
line.setFill(gridColor);
line.setTranslateY(y);
line.setTranslateZ(zTranslate);
line.setStrokeWidth(lineWidth);
getChildren().addAll(line);
}
for (int x = 0; x <= size; x += size / 10) {
Line line = new Line(0, 0, 0, size);
line.setStroke(gridColor);
line.setFill(gridColor);
line.setTranslateX(x);
line.setTranslateZ(zTranslate);
line.setStrokeWidth(lineWidth);
getChildren().addAll(line);
}
}
public void setFill(Paint paint) {
wall.setFill(paint);
}
}
public void makeZoomable(StackPane control) {
final double MAX_SCALE = 20.0;
final double MIN_SCALE = 0.1;
control.addEventFilter(ScrollEvent.ANY, new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double delta = 1.2;
double scale = control.getScaleX();
if (event.getDeltaY() < 0) {
scale /= delta;
} else {
scale *= delta;
}
scale = clamp(scale, MIN_SCALE, MAX_SCALE);
control.setScaleX(scale);
control.setScaleY(scale);
event.consume();
}
});
}
/**
* Create axis walls
*
* #param size
* #return
*/
private Group createGrid(int size) {
Group cube = new Group();
// size of the cube
Color color = Color.LIGHTGRAY;
List<Axis> cubeFaces = new ArrayList<>();
Axis r;
// back face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.5 * 1), 1.0));
r.setTranslateX(-0.5 * size);
r.setTranslateY(-0.5 * size);
r.setTranslateZ(0.5 * size);
cubeFaces.add(r);
// bottom face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.4 * 1), 1.0));
r.setTranslateX(-0.5 * size);
r.setTranslateY(0);
r.setRotationAxis(Rotate.X_AXIS);
r.setRotate(90);
cubeFaces.add(r);
// right face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.3 * 1), 1.0));
r.setTranslateX(-1 * size);
r.setTranslateY(-0.5 * size);
r.setRotationAxis(Rotate.Y_AXIS);
r.setRotate(90);
// cubeFaces.add( r);
// left face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.2 * 1), 1.0));
r.setTranslateX(0);
r.setTranslateY(-0.5 * size);
r.setRotationAxis(Rotate.Y_AXIS);
r.setRotate(90);
cubeFaces.add(r);
// top face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.1 * 1), 1.0));
r.setTranslateX(-0.5 * size);
r.setTranslateY(-1 * size);
r.setRotationAxis(Rotate.X_AXIS);
r.setRotate(90);
// cubeFaces.add( r);
// front face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.1 * 1), 1.0));
r.setTranslateX(-0.5 * size);
r.setTranslateY(-0.5 * size);
r.setTranslateZ(-0.5 * size);
// cubeFaces.add( r);
cube.getChildren().addAll(cubeFaces);
return cube;
}
public static double normalizeValue(double value, double min, double max, double newMin, double newMax) {
return (value - min) * (newMax - newMin) / (max - min) + newMin;
}
public static double clamp(double value, double min, double max) {
if (Double.compare(value, min) < 0)
return min;
if (Double.compare(value, max) > 0)
return max;
return value;
}
public static Color randomColor() {
return Color.rgb(rnd.nextInt(255), rnd.nextInt(255), rnd.nextInt(255));
}
public static void main(String[] args) {
launch(args);
}
}

Here's a basic idea to create some measures on the axes. It is not production-ready but should give you enough to start with.
private Group createGrid(int size) {
// existing code omitted...
cube.getChildren().addAll(cubeFaces);
double gridSizeHalf = size / 2;
double labelOffset = 30 ;
double labelPos = gridSizeHalf - labelOffset ;
for (double coord = -gridSizeHalf ; coord < gridSizeHalf ; coord+=50) {
Text xLabel = new Text(coord, labelPos, String.format("%.0f", coord));
xLabel.setTranslateZ(labelPos);
xLabel.setScaleX(-1);
Text yLabel = new Text(labelPos, coord, String.format("%.0f", coord));
yLabel.setTranslateZ(labelPos);
yLabel.setScaleX(-1);
Text zLabel = new Text(labelPos, labelPos, String.format("%.0f", coord));
zLabel.setTranslateZ(coord);
cube.getChildren().addAll(xLabel, yLabel, zLabel);
zLabel.setScaleX(-1);
}
return cube;
}
I would just place a legend outside the graph, which would just be a 2D grid pane not rotating...

I know this question is getting old but 2D labels in a JavaFX 3D scene is a topic that comes up a lot and I never see it answered "the right way".
Translating the labels like in James_D's answer will translate into 3D space a 2D label which will look correct until you move the camera. Assuming you want a scatter chart that doesn't move or rotate then this will be fine. Other wise you will need to automatically transform the 2D labels whenever you move your camera. (ie... the mouse handler). You could remove your scatter chart and readd the whole thing to the scene each time but that will be murder on your heap memory and won't be feasible for data sets of any real useful size.
The right way to do it is to use OpenGL or DirectDraw text renders which redraw the labels on each render loop pass but JavaFX 3D doesn't give you access (currently). So the "right way in JavaFX" is to float 2D labels on top of a 3D subscene and then translate them whenever the camera moves. This requires that you transform the 3D coordinate projection of the 3D location you want the label to a 2D screen projection.
To generically manage 2D labels connected to a Point3D in JavaFX 3D you need to do a transform along the following:
Point3D coordinates = node.localToScene(javafx.geometry.Point3D.ZERO);
SubScene oldSubScene = NodeHelper.getSubScene(node);
coordinates = SceneUtils.subSceneToScene(oldSubScene, coordinates);
double x = coordinates.getX();
double y = coordinates.getY();
label.getTransforms().setAll(new Translate(x, y));
Where the node is some actual 3D object already in the 3D subscene. For my applications I simply use a Sphere of an extremely small size it cannot be seen. If you were to follow James_D's example, you could translate the sphere(s) to the same locations that you translated the original axis labels.
The label is a standard JavaFX 2D label that you add to your scene... typically through a StackPane such that the labels are floating on top of the 3D subscene.
Now whenever the camera moves/rotates, this causes this transform to be called which slides the label on the 2D layer. Without direct access to the underlying GL or DD calls this is pretty much the only way to do something like this in JavaFX 3D but it works pretty well.
Here is a video example of it working.
Here is an open source example of implementing a simple version of floating 2D labels. (Warning, I'm the contributing author for the sample, not trying to promote the library.)

Related

Centroid of a javafx Shape

How can one find the centroid of a javafx Shape object?
Longer story: I am highlighting the intersections of 2 or more shapes, and need to position text over this intersection - ideally centering the text at the centroid. Given the potential for these intersections to be irregular, using the center of the intersection's layout bounds for calculations quite often result in mis-positioning of the text. For example, constructing the code below results in the center point that is offset relative to the conceptual center:
public class Test extends Application{
public static void main(String[] args) {
launch(args);
}
public void start(Stage primaryStage){
/*
* Create shapes
*/
Circle s1 = new Circle(100, 100, 75);
s1.setFill(new Color(1, 0, 0, 0.25));
Circle s2 = new Circle(200, 100, 75);
s2.setFill(new Color(0, 1, 0, 0.25));
Circle s3 = new Circle(150, 125, 50);
s3.setFill(new Color(0, 0, 1, 0.25));
/*
* Create the intersection
*/
Shape intersection = Shape.intersect(s3, s2);
intersection = Shape.subtract(intersection, s1);
intersection.setStroke(Color.RED);
/*
* Create a point that shows the layout bounds center
*/
Circle boundsCenter = new Circle();
boundsCenter.setRadius(2d);
boundsCenter.setCenterX(intersection.getLayoutBounds().getMinX() + intersection.getLayoutBounds().getWidth()/2);
boundsCenter.setCenterY(intersection.getLayoutBounds().getMinY() + intersection.getLayoutBounds().getHeight()/2);
boundsCenter.setFill(Color.RED);
intersection.setFill(null);
Pane pane = new Pane();
pane.getChildren().addAll(s1, s2, s3, intersection, boundsCenter);
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.sizeToScene();
primaryStage.show();
}
}
I would prefer to rely on the javafx API to do so (java 8 compatible), if at all possible.
One Option I have considered is to estimate the centroid by sampling using the Shape API and calculating the centroid of Points that fall within the given Shape:
//estimate the centroid by random sampling
int maxEstimate = 100;
double x = 0; double y = 0; double count = 0;
while(count < maxEstimate) {
double randomX = intersection.getLayoutBounds().getMinX() + Math.random() * intersection.getLayoutBounds().getWidth();
double randomY = intersection.getLayoutBounds().getMinY() + Math.random() * intersection.getLayoutBounds().getHeight();
Circle test = new Circle(randomX, randomY, 1d);
Shape isInside = Shape.intersect(c, intersection);
if ( isInside.getLayoutBounds().getWidth() > 0 || isInside.getLayoutBounds().getHeight() > 0 ) {
x += randomX;
y += randomY;
count++;
}
}
double centroidX = x / count;
double centroidY = y / count;
The problem with this method is that it is non-deterministic, can be inaccurate for smaller number of iterations, and quite intensive and time consuming for a larger number of shapes (and/or a more accurate measurement)

how to add numbers to clock from 1 to 12, i just create circle

import java.awt.Color;
import acm.graphics.GOval;
import acm.program.GraphicsProgram;
public class Clock extends GraphicsProgram {
private static final long serialVersionUID = 1L;
public void run() {
GOval tofig = createFilledCircle(getWidth()/2, getHeight()/2, 200, Color.black);
add(tofig);
GOval lala = createFilledCircle(getWidth()/2, getHeight()/2, 180, Color.white);
add(lala);
}
private GOval createFilledCircle(double x, double y, double r, Color color) {
GOval circle = new GOval(x - r, y - r, 2 * r, 2 * r);
circle.setFilled(true);
circle.setColor(color);
return circle;
}
// Ignore this;
public static void main(String[] args) {
new Clock().start();
}
}
Here's the code for the trigonometry part of what you're trying to do that you and I have kinda worked on together:
public class DrawCircle {
static final double twopi = Math.PI * 2;
static final double fudge = 0.000001;
private static void drawHourLabels(double center_x, double center_y, double radius) {
int steps = 12;
for (double angle = 0.0; angle < (twopi - fudge); angle += twopi/steps) {
double x_offset = Math.sin(angle) * radius;
double y_offset = Math.cos(angle) * radius;
double x = center_x + x_offset;
double y = center_y + y_offset;
// Here you'd do the actual drawing of each hour label at the coordinates x,y. We'll just print them for now.
System.out.println(String.format("%f %f", x, y));
}
}
public static void main(String... args) {
// drawHourLabels(getWidth()/2, getHeight()/2, 220); // <-- you'd do something like this in your "run" method.
// Draw clock labels around circle with center at 400x200 of radius 220
drawHourLabels(400, 600, 220);
}
}
The 'fudge' value is used because floating point arithmetic isn't perfectly precise. By the time we've added 12 floating point values together to get to 2 * Math.PI, we might be a little over or under. We want to make sure we don't process the 12:00 position again at the end of the loop because we computed a value just a little smaller than 2 * Math.PI. So we add a "fudge factor" that's really small, but guaranteed to be bigger than any floating point inaccuracy we've accumulated.
Take a look at this simple example from me.
/**
* Draw text elements from 1 to 12 inside a circle
* #return a group of text elements from 1 to 12
*/
public strictfp static Group drawText() {
//a list for storing text numbers
List<Text> numbersInClock = new ArrayList<>();
//create a group of shapes
Group group = new Group();
//set it to x,y positions
group.setLayoutX(150);
group.setLayoutY(150);
//numbers corresponding to angle calculations
int[] numbers = {3,4,5,6,7,8,9,10,11,12,1,2};
double[] angles = {0, 0.166666666667, 0.333333333334,0.5, 0.666666666667, 0.833333333334, 1, 1.166666666667, 1.33333333334, 1.5, 1.666666666667, 1.83333333334};
int i = 0;
for (double angle : angles) {
//Calculated formula for x,y positions of each number within a circle
//(x,y) = (rcos(angle),rsin(angle))
int r = 90; // length of radius, a bit shorter to put it inside a circle
//calculate x and y positions based on formula for numbers within a circle
double x = r * Math.cos(angle*Math.PI);
double y = r * Math.sin(angle*Math.PI);
//create a text element consiting a coressponding number
Text text = new Text(x, y, String.valueOf(numbers[i++]));
//add it to a list
numbersInClock.add(text);
}
//add all text elements to a group
group.getChildren().addAll(numbersInClock);
//return a group
return group;
}
Full code can be acquired from here: https://github.com/MomirSarac/JavaFX-Circle-Clock-Image
You can use sin and cos to know where to set your text.
Sine and Cosine are two math operators that take in an angle in radians and give out number beetween -1 and 1 to know how to multiply to get coordinates
you can learn more here

JavaFX: How to resize a rectangle with set aspect ratio

I have an app where user can draw a rectangle, and resize it or move it. I'm interested if is possible to some how bind rectangles width and height in some aspect ratio.
E.g. if aspect ratio is 1:2 that user can draw only that kinds of rectangles, or if is 1:1 that user can only draw square.
EDIT
My eventHandler for MOUSE_DRAGGEDevent looks something like this
EventHandler<MouseEvent> onMouseDraggedEventHandler = event -> {
if (event.isSecondaryButtonDown())
return;
double offsetX = event.getX() - rectangleStartX;
double offsetY = event.getY() - rectangleStartY;
if (offsetX > 0) {
if (event.getX() > imageView.getFitWidth()) {
selectionRectangle.setWidth(imageView.getFitWidth() - rectangleStartX);
} else
selectionRectangle.setWidth(offsetX);
} else {
if (event.getX() < 0)
selectionRectangle.setX(0);
else
selectionRectangle.setX(event.getX());
selectionRectangle.setWidth(rectangleStartX - selectionRectangle.getX());
}
if (offsetY > 0) {
if (event.getY() > imageView.getFitHeight())
selectionRectangle.setHeight(imageView.getFitHeight() - rectangleStartY);
else
selectionRectangle.setHeight(offsetY);
} else {
if (event.getY() < 0)
selectionRectangle.setY(0);
else
selectionRectangle.setY(event.getY());
selectionRectangle.setHeight(rectangleStartY - selectionRectangle.getY());
}
};
This app demos how to adjust a Rectangle height and width based on a ratio. Comments in code.
import javafx.application.Application;
import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
/**
*
* #author blj0011
*/
public class JavaFXApplication122 extends Application
{
double orgSceneX, orgSceneY;//Used to help keep up with change in mouse position
#Override
public void start(Stage primaryStage)
{
double RATIO = .5;//The ration of height to width is 1/2
Rectangle rectangle = new Rectangle(100, 50);
rectangle.setX(400 - 50);
rectangle.setY(250 - 25);
rectangle.setFill(Color.TRANSPARENT);
rectangle.setStroke(Color.BLACK);
//Circles will be used to do the event handling/movements
Circle leftAnchor = new Circle(400 - 50, 250, 5);
Circle topAnchor = new Circle(400, 250 - 25, 5);
leftAnchor.setOnMouseDragEntered((event) -> {
((Circle) event.getSource()).getScene().setCursor(Cursor.MOVE);
});
leftAnchor.setOnMousePressed((event) -> {
orgSceneX = event.getSceneX();//Store current mouse position
});
leftAnchor.setOnMouseDragged((event) -> {
double offSetX = event.getSceneX() - orgSceneX;//Find change in mouse X position
leftAnchor.setCenterX(event.getSceneX());
rectangle.setX(event.getSceneX());//move rectangle left side with mouse
rectangle.setWidth(rectangle.getWidth() - offSetX);//Change rectangle's width with movement of mouse
topAnchor.setCenterX(topAnchor.getCenterX() + offSetX / 2);//Adjust top circle as rectangle's size change
rectangle.setHeight(rectangle.getWidth() * RATIO);//Change the height so that it meets the ratio requirements
leftAnchor.setCenterY((rectangle.getY() + rectangle.getHeight()) - (rectangle.getHeight() / 2));//Adjust the left circle with the growth of the rectangle
orgSceneX = event.getSceneX();//save last mouse position to recalculate change in mouse postion as the circle moves
});
leftAnchor.setOnMouseExited((event) -> {
leftAnchor.getScene().setCursor(null);
});
topAnchor.setOnMouseDragEntered((event) -> {
topAnchor.getScene().setCursor(Cursor.MOVE);
});
topAnchor.setOnMousePressed((event) -> {
orgSceneY = event.getSceneY();//store current mouse position
});
topAnchor.setOnMouseDragged((event) -> {
double offSetY = event.getSceneY() - orgSceneY;
topAnchor.setCenterY(event.getSceneY());
rectangle.setY(event.getSceneY());//move rectangle top side with mouse
rectangle.setHeight(rectangle.getHeight() - offSetY);//Change rectangle's height with movement of mouse
leftAnchor.setCenterY(leftAnchor.getCenterY() + offSetY / 2);//Adjust left circle as rectangle's size change
rectangle.setWidth(rectangle.getHeight() * (1 / RATIO));//Change the width so that it meets the ratio requirements
topAnchor.setCenterX((rectangle.getX() + rectangle.getWidth()) - (rectangle.getWidth() / 2));//Adjust the top circle with the growth of the rectangle
orgSceneY = event.getSceneY();//save last mouse position to recalculate change in mouse postion as the circle moves
});
topAnchor.setOnMouseExited((event) -> {
topAnchor.getScene().setCursor(null);
});
Pane root = new Pane();
root.getChildren().addAll(rectangle, leftAnchor, topAnchor);
Scene scene = new Scene(root, 800, 500);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args)
{
launch(args);
}
}

bullet trajectory

First of all, I want to shoot a plane with a cannon.
I've setted this Timeline for the trajectory, but I don't see the bullet on my Scene. It's very likely that my trajectory's code isn't correct. I tried to look on the internet about formula for projectile motion, but I understand nothing about physics;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Bounds;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Game_1 extends Application {
private final double gravity = 9.81;
private Timeline timeline;
private ImageView plane;
private Circle circle;
private AnchorPane ap;
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Test");
Group group = new Group();
Scene scene = new Scene(group, 600, 350);
scene.setFill(Color.BLACK);
primaryStage.setScene(scene);
primaryStage.show();
}
private void shoot() {
double x = 65.0f;
double y = 408;
double speed = 200;
double t = 2;
double angle = -45;
double dx = Math.cos(angle) * speed;
double dy = Math.sin(angle) * speed;
circle = new Circle(x, y, 5, Color.BLACK);
double x2 = x + dx * t;
double y2 = (Math.tan(angle) * y - (gravity / (2 * Math.pow(speed, 2) * Math.cos(angle))) * Math.pow(x, 2));
timeline = new Timeline();
KeyValue xKV = new KeyValue(circle.centerXProperty(), x2);
KeyValue yKV = new KeyValue(circle.centerYProperty(), y2, new Interpolator() {
#Override
protected double curve(double t) {
return y + dy * t - 0.5 * gravity * t * t;
}
});
KeyFrame xKF = new KeyFrame(Duration.seconds(t), xKV);
KeyFrame yKF = new KeyFrame(Duration.seconds(t), yKV);
timeline.getKeyFrames().addAll(xKF, yKF);
ap.getChildren().add(circle);
timeline.play();
collision();
}
private void collision() {
circle.boundsInParentProperty().addListener((ObservableValue<? extends Bounds> arg0, Bounds oldValue2, Bounds newValue2) -> {
if (circle.getBoundsInParent().intersects(plane.getBoundsInParent())) {
timeline.stop();
ap.getChildren().remove(circle);
}
});
}
}
The curve method should map to the interval [0, 1]. Your method however maps to much higher values. The value val at time t of a animation from t0 to t1 for a interpolator i given start value val0 and end value val1 is calculated as follows:
val = val0 + (val1 - val0) * i.curve((t - t0) / (t1 - t0))
The parameter of the curve method is the relative position in the time interval (0 = start of animation; 1 = end of animation). The result of the method is used to determine how close the value is to the end value (0 = still at the start value; 1 = at the end value).
Therefore you should probably calculate the top point hMax in the cannonball's curve (as described e.g. here on Wikipedia) and use a different interpolator:
Interpolator interpolator = new Interpolator() {
#Override
protected double curve(double t) {
// parabola with zeros at t=0 and t=1 and a maximum of 1 at t=0.5
return 4 * t * (1 - t);
}
};
KeyValue yKV = new KeyValue(circle.centerYProperty(), hMax, interpolator);
Note that upward movement means decreasing the y coordinate for the UI so in this case hMax should be smaller than the y value at the start.
Appart from that your shoot method is never called and some fields are not initialized which would result in a NPE in case it was called. Furthermore if those 2 issues are fixed, a black circle on a black background will be hard to see...
Example
Note that this is not using any physical fromulae and instead just uses some values chosen by me:
#Override
public void start(Stage primaryStage) {
Circle circle = new Circle(10);
circle.setManaged(false);
Pane pane = new Pane(circle);
circle.setCenterX(20);
circle.setCenterY(800);
Timeline timeline = new Timeline(new KeyFrame(Duration.ZERO,
new KeyValue(circle.centerXProperty(), 20),
new KeyValue(circle.centerYProperty(), 800)
), new KeyFrame(Duration.seconds(3),
new KeyValue(circle.centerXProperty(), 380),
new KeyValue(circle.centerYProperty(), 10, new Interpolator() {
#Override
protected double curve(double t) {
// parabola with zeros at t=0 and t=1 and a maximum of 1 at t=0.5
return 4 * t * (1 - t);
}
})
)
);
Scene scene = new Scene(pane, 400, 800);
scene.setOnMouseClicked(evt -> timeline.playFromStart());
primaryStage.setScene(scene);
primaryStage.show();
}
Note that Interpolator.curve is supposed to return 0 for parameter 0 and 1 for parameter 1. Anything else will probably result in jumps, should the property be animated further. Maybe the y-movement in 2 parts would be more appropriate, in case you want to move the ball around after the animation is finished.
I.e.
Interpolator 1: t * (2 - t)
Interpolator 2: t * t
using half the time interval each with end values of the top and start y coordinate of the curve respectively.

WorldWind line of sight

I've found this example of how to render line of sight in WorldWind: http://patmurris.blogspot.com/2008/04/ray-casting-and-line-of-sight-for-wwj.html (its a bit old, but it still seems to work). This is the class used in the example (slightly modified code below to work with WorldWind 2.0). It looks like the code also uses RayCastingSupport (Javadoc and Code) to do its magic.
What I'm trying to figure out is if this code/example is using the curvature of the earth/and or the distance to the horizon as part of its logic. Just looking at the code, I'm not sure I understand completely what it is doing.
For instance, if I was trying to figure out what terrain a person 200 meters above the earth could "see", would it take the distance to the horizon into account?
What would it take to modify the code to account for distance to the horizon/curvature of the earth (if its not already)?
package gov.nasa.worldwindx.examples;
import gov.nasa.worldwind.util.RayCastingSupport;
import gov.nasa.worldwind.view.orbit.OrbitView;
import gov.nasa.worldwind.geom.Angle;
import gov.nasa.worldwind.geom.Position;
import gov.nasa.worldwind.geom.Sector;
import gov.nasa.worldwind.geom.Vec4;
import gov.nasa.worldwind.globes.Globe;
import gov.nasa.worldwind.layers.CrosshairLayer;
import gov.nasa.worldwind.layers.RenderableLayer;
import gov.nasa.worldwind.render.*;
import javax.swing.*;
import javax.swing.border.CompoundBorder;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
public class LineOfSight extends ApplicationTemplate
{
public static class AppFrame extends ApplicationTemplate.AppFrame
{
private double samplingLength = 30; // Ray casting sample length
private int centerOffset = 100; // meters above ground for center
private int pointOffset = 10; // meters above ground for sampled points
private Vec4 light = new Vec4(1, 1, -1).normalize3(); // Light direction (from South-East)
private double ambiant = .4; // Minimum lighting (0 - 1)
private RenderableLayer renderableLayer;
private SurfaceImage surfaceImage;
private ScreenAnnotation screenAnnotation;
private JComboBox radiusCombo;
private JComboBox samplesCombo;
private JCheckBox shadingCheck;
private JButton computeButton;
public AppFrame()
{
super(true, true, false);
// Add USGS Topo Maps
// insertBeforePlacenames(getWwd(), new USGSTopographicMaps());
// Add our renderable layer for result display
this.renderableLayer = new RenderableLayer();
this.renderableLayer.setName("Line of sight");
this.renderableLayer.setPickEnabled(false);
insertBeforePlacenames(getWwd(), this.renderableLayer);
// Add crosshair layer
insertBeforePlacenames(getWwd(), new CrosshairLayer());
// Update layer panel
this.getLayerPanel().update(getWwd());
// Add control panel
this.getLayerPanel().add(makeControlPanel(), BorderLayout.SOUTH);
}
private JPanel makeControlPanel()
{
JPanel controlPanel = new JPanel(new GridLayout(0, 1, 0, 0));
controlPanel.setBorder(
new CompoundBorder(BorderFactory.createEmptyBorder(9, 9, 9, 9),
new TitledBorder("Line Of Sight")));
// Radius combo
JPanel radiusPanel = new JPanel(new GridLayout(0, 2, 0, 0));
radiusPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
radiusPanel.add(new JLabel("Max radius:"));
radiusCombo = new JComboBox(new String[] {"5km", "10km",
"20km", "30km", "50km", "100km", "200km"});
radiusCombo.setSelectedItem("10km");
radiusPanel.add(radiusCombo);
// Samples combo
JPanel samplesPanel = new JPanel(new GridLayout(0, 2, 0, 0));
samplesPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
samplesPanel.add(new JLabel("Samples:"));
samplesCombo = new JComboBox(new String[] {"128", "256", "512"});
samplesCombo.setSelectedItem("128");
samplesPanel.add(samplesCombo);
// Shading checkbox
JPanel shadingPanel = new JPanel(new GridLayout(0, 2, 0, 0));
shadingPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
shadingPanel.add(new JLabel("Light:"));
shadingCheck = new JCheckBox("Add shading");
shadingCheck.setSelected(false);
shadingPanel.add(shadingCheck);
// Compute button
JPanel buttonPanel = new JPanel(new GridLayout(0, 1, 0, 0));
buttonPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
computeButton = new JButton("Compute");
computeButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent actionEvent)
{
update();
}
});
buttonPanel.add(computeButton);
// Help text
JPanel helpPanel = new JPanel(new GridLayout(0, 1, 0, 0));
buttonPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
helpPanel.add(new JLabel("Place view center on an elevated"));
helpPanel.add(new JLabel("location and click \"Compute\""));
// Panel assembly
controlPanel.add(radiusPanel);
controlPanel.add(samplesPanel);
controlPanel.add(shadingPanel);
controlPanel.add(buttonPanel);
controlPanel.add(helpPanel);
return controlPanel;
}
// Update line of sight computation
private void update()
{
new Thread(new Runnable() {
public void run()
{
computeLineOfSight();
}
}, "LOS thread").start();
}
private void computeLineOfSight()
{
computeButton.setEnabled(false);
computeButton.setText("Computing...");
try
{
Globe globe = getWwd().getModel().getGlobe();
OrbitView view = (OrbitView)getWwd().getView();
Position centerPosition = view.getCenterPosition();
// Compute sector
String radiusString = ((String)radiusCombo.getSelectedItem());
double radius = 1000 * Double.parseDouble(radiusString.substring(0, radiusString.length() - 2));
double deltaLatRadians = radius / globe.getEquatorialRadius();
double deltaLonRadians = deltaLatRadians / Math.cos(centerPosition.getLatitude().radians);
Sector sector = new Sector(centerPosition.getLatitude().subtractRadians(deltaLatRadians),
centerPosition.getLatitude().addRadians(deltaLatRadians),
centerPosition.getLongitude().subtractRadians(deltaLonRadians),
centerPosition.getLongitude().addRadians(deltaLonRadians));
// Compute center point
double centerElevation = globe.getElevation(centerPosition.getLatitude(),
centerPosition.getLongitude());
Vec4 center = globe.computePointFromPosition(
new Position(centerPosition, centerElevation + centerOffset));
// Compute image
float hueScaleFactor = .7f;
int samples = Integer.parseInt((String)samplesCombo.getSelectedItem());
BufferedImage image = new BufferedImage(samples, samples, BufferedImage.TYPE_4BYTE_ABGR);
double latStepRadians = sector.getDeltaLatRadians() / image.getHeight();
double lonStepRadians = sector.getDeltaLonRadians() / image.getWidth();
for (int x = 0; x < image.getWidth(); x++)
{
Angle lon = sector.getMinLongitude().addRadians(lonStepRadians * x + lonStepRadians / 2);
for (int y = 0; y < image.getHeight(); y++)
{
Angle lat = sector.getMaxLatitude().subtractRadians(latStepRadians * y + latStepRadians / 2);
double el = globe.getElevation(lat, lon);
// Test line of sight from point to center
Vec4 point = globe.computePointFromPosition(lat, lon, el + pointOffset);
double distance = point.distanceTo3(center);
if (distance <= radius)
{
if (RayCastingSupport.intersectSegmentWithTerrain(
globe, point, center, samplingLength, samplingLength) == null)
{
// Center visible from point: set pixel color and shade
float hue = (float)Math.min(distance / radius, 1) * hueScaleFactor;
float shade = shadingCheck.isSelected() ?
(float)computeShading(globe, lat, lon, light, ambiant) : 0f;
image.setRGB(x, y, Color.HSBtoRGB(hue, 1f, 1f - shade));
}
else if (shadingCheck.isSelected())
{
// Center not visible: apply shading nonetheless if selected
float shade = (float)computeShading(globe, lat, lon, light, ambiant);
image.setRGB(x, y, new Color(0f, 0f, 0f, shade).getRGB());
}
}
}
}
// Blur image
PatternFactory.blur(PatternFactory.blur(PatternFactory.blur(PatternFactory.blur(image))));
// Update surface image
if (this.surfaceImage != null)
this.renderableLayer.removeRenderable(this.surfaceImage);
this.surfaceImage = new SurfaceImage(image, sector);
this.surfaceImage.setOpacity(.5);
this.renderableLayer.addRenderable(this.surfaceImage);
// Compute distance scale image
BufferedImage scaleImage = new BufferedImage(64, 256, BufferedImage.TYPE_4BYTE_ABGR);
Graphics g2 = scaleImage.getGraphics();
int divisions = 10;
int labelStep = scaleImage.getHeight() / divisions;
for (int y = 0; y < scaleImage.getHeight(); y++)
{
int x1 = scaleImage.getWidth() / 5;
if (y % labelStep == 0 && y != 0)
{
double d = radius / divisions * y / labelStep / 1000;
String label = Double.toString(d) + "km";
g2.setColor(Color.BLACK);
g2.drawString(label, x1 + 6, y + 6);
g2.setColor(Color.WHITE);
g2.drawLine(x1, y, x1 + 4 , y);
g2.drawString(label, x1 + 5, y + 5);
}
float hue = (float)y / (scaleImage.getHeight() - 1) * hueScaleFactor;
g2.setColor(Color.getHSBColor(hue, 1f, 1f));
g2.drawLine(0, y, x1, y);
}
// Update distance scale screen annotation
if (this.screenAnnotation != null)
this.renderableLayer.removeRenderable(this.screenAnnotation);
this.screenAnnotation = new ScreenAnnotation("", new Point(20, 20));
this.screenAnnotation.getAttributes().setImageSource(scaleImage);
this.screenAnnotation.getAttributes().setSize(
new Dimension(scaleImage.getWidth(), scaleImage.getHeight()));
this.screenAnnotation.getAttributes().setAdjustWidthToText(Annotation.SIZE_FIXED);
this.screenAnnotation.getAttributes().setDrawOffset(new Point(scaleImage.getWidth() / 2, 0));
this.screenAnnotation.getAttributes().setBorderWidth(0);
this.screenAnnotation.getAttributes().setCornerRadius(0);
this.screenAnnotation.getAttributes().setBackgroundColor(new Color(0f, 0f, 0f, 0f));
this.renderableLayer.addRenderable(this.screenAnnotation);
// Redraw
this.getWwd().redraw();
}
finally
{
computeButton.setEnabled(true);
computeButton.setText("Compute");
}
}
/**
* Compute shadow intensity at a globe position.
* #param globe the <code>Globe</code>.
* #param lat the location latitude.
* #param lon the location longitude.
* #param light the light direction vector. Expected to be normalized.
* #param ambiant the minimum ambiant light level (0..1).
* #return the shadow intensity for the location. No shadow = 0, totaly obscured = 1.
*/
private static double computeShading(Globe globe, Angle lat, Angle lon, Vec4 light, double ambiant)
{
double thirtyMetersRadians = 30 / globe.getEquatorialRadius();
Vec4 p0 = globe.computePointFromPosition(lat, lon, 0);
Vec4 px = globe.computePointFromPosition(lat, Angle.fromRadians(lon.radians - thirtyMetersRadians), 0);
Vec4 py = globe.computePointFromPosition(Angle.fromRadians(lat.radians + thirtyMetersRadians), lon, 0);
double el0 = globe.getElevation(lat, lon);
double elx = globe.getElevation(lat, Angle.fromRadians(lon.radians - thirtyMetersRadians));
double ely = globe.getElevation(Angle.fromRadians(lat.radians + thirtyMetersRadians), lon);
Vec4 vx = new Vec4(p0.distanceTo3(px), 0, elx - el0).normalize3();
Vec4 vy = new Vec4(0, p0.distanceTo3(py), ely - el0).normalize3();
Vec4 normal = vx.cross3(vy).normalize3();
return 1d - Math.max(-light.dot3(normal), ambiant);
}
}
public static void main(String[] args)
{
ApplicationTemplate.start("World Wind Line Of Sight Calculation", AppFrame.class);
}
}
You are correct. This code does not take into account the earth curve.
From what I could see, a ray trace is done for the center of the light but the cone of the light was drawn on an image (I am not sure about that, but it looks as if this example draws on an image of gray scale).
Any way this demo is about detecting hitting the ground to stop the ray trace.
From what I understand, the algorithm stops after a distance set in the form (5km,10km ... 200km etc.)
I don't understand the direction of the ray. It makes sense to check for 200km radius only if you check light from out of space....
If you want to take the horizon into account you should check the pitch of the light source first. Its relevant for positive pitch values (above the horizon).
In that case you should decide when to stop once the center of the light gets very high above ground. How high depends on whether you point your light towards a mountain slope of you terrain is relatively flat, or if the source of light is narrow beam or wide.

Categories

Resources