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.
Related
I'm drawing two shapes (circles) in a JPanel and I need to connect them with a line. I was doing this by just getting the middle point of the circle and connecting each other, easy.
The problem is that now I need to make single-direction lines, which has an "arrow" at the end, to point out which direction the line goes. So now I can't use the middle point of the circle because I need to connect each other from border to border, so the "arrow' can appear correctly.
On my last try that was the result, nothing good:
PS: In the screenshot I'm not filling the circles just to see the exact position of the line, but normally I would fill it.
I'm having trouble to calculate the exact position of the border I need to start/end my line. Anyone has any idea on how to do this?
EDIT: The circles are movable, they could be in any position, so the line should work in any case.
Okay, so basically, we can break down the problem to basic issues:
Get the angle between the two circles
Draw a line from circumference of one circle to another along this angle
Both these issues aren't hard to solve (and any time spent searching the internet would provide solutions - because that's where I got them from ;))
So, the angle between two points could be calculated using something like...
protected double angleBetween(Point2D from, Point2D to) {
double x = from.getX();
double y = from.getY();
// This is the difference between the anchor point
// and the mouse. Its important that this is done
// within the local coordinate space of the component,
// this means either the MouseMotionListener needs to
// be registered to the component itself (preferably)
// or the mouse coordinates need to be converted into
// local coordinate space
double deltaX = to.getX() - x;
double deltaY = to.getY() - y;
// Calculate the angle...
// This is our "0" or start angle..
double rotation = -Math.atan2(deltaX, deltaY);
rotation = Math.toRadians(Math.toDegrees(rotation) + 180);
return rotation;
}
And the point on a circle can be calculated using something like...
protected Point2D getPointOnCircle(Point2D center, double radians, double radius) {
double x = center.getX();
double y = center.getY();
radians = radians - Math.toRadians(90.0); // 0 becomes the top
// Calculate the outter point of the line
double xPosy = Math.round((float) (x + Math.cos(radians) * radius));
double yPosy = Math.round((float) (y + Math.sin(radians) * radius));
return new Point2D.Double(xPosy, yPosy);
}
Just beware, there's some internal modifications of the results to allow for the difference between the mathematical solution and the way that the Graphics API draws circles
Okay, so big deal you say, how does that help me? Well, I great deal actually.
You'd calculate the angle between the to circles (both to and from, you might be able to simple inverse one angle, but I have the calculation available so I used it). From that, you can calculate the point on each circle where the line will intersect and then you simply need to draw it, something like...
double from = angleBetween(circle1, circle2);
double to = angleBetween(circle2, circle1);
Point2D pointFrom = getPointOnCircle(circle1, from);
Point2D pointTo = getPointOnCircle(circle2, to);
Line2D line = new Line2D.Double(pointFrom, pointTo);
g2d.draw(line);
Runnable Example
Because I've distilled much of the calculations down to communalised properties, I've provided my test code as a runnable example. All the calculations are based on dynamic values, nothing is really hard coded. For example, you can change the size and positions of the circles and the calculations should continue to work...
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private Ellipse2D circle1;
private Ellipse2D circle2;
private Point2D drawTo;
public TestPane() {
circle1 = new Ellipse2D.Double(10, 10, 40, 40);
circle2 = new Ellipse2D.Double(100, 150, 40, 40);
//addMouseMotionListener(new MouseAdapter() {
// #Override
// public void mouseMoved(MouseEvent e) {
// drawTo = new Point2D.Double(e.getPoint().x, e.getPoint().y);
// repaint();
// }
//});
}
protected Point2D center(Rectangle2D bounds) {
return new Point2D.Double(bounds.getCenterX(), bounds.getCenterY());
}
protected double angleBetween(Shape from, Shape to) {
return angleBetween(center(from.getBounds2D()), center(to.getBounds2D()));
}
protected double angleBetween(Point2D from, Point2D to) {
double x = from.getX();
double y = from.getY();
// This is the difference between the anchor point
// and the mouse. Its important that this is done
// within the local coordinate space of the component,
// this means either the MouseMotionListener needs to
// be registered to the component itself (preferably)
// or the mouse coordinates need to be converted into
// local coordinate space
double deltaX = to.getX() - x;
double deltaY = to.getY() - y;
// Calculate the angle...
// This is our "0" or start angle..
double rotation = -Math.atan2(deltaX, deltaY);
rotation = Math.toRadians(Math.toDegrees(rotation) + 180);
return rotation;
}
protected Point2D getPointOnCircle(Shape shape, double radians) {
Rectangle2D bounds = shape.getBounds();
// Point2D point = new Point2D.Double(bounds.getX(), bounds.getY());
Point2D point = center(bounds);
return getPointOnCircle(point, radians, Math.max(bounds.getWidth(), bounds.getHeight()) / 2d);
}
protected Point2D getPointOnCircle(Point2D center, double radians, double radius) {
double x = center.getX();
double y = center.getY();
radians = radians - Math.toRadians(90.0); // 0 becomes th?e top
// Calculate the outter point of the line
double xPosy = Math.round((float) (x + Math.cos(radians) * radius));
double yPosy = Math.round((float) (y + Math.sin(radians) * radius));
return new Point2D.Double(xPosy, yPosy);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.draw(circle1);
g2d.draw(circle2);
// This was used for testing, it will draw a line from circle1 to the
// drawTo point, which, if enabled, is the last known position of the
// mouse
//if (drawTo != null) {
// Point2D pointFrom = center(circle1.getBounds2D());
// g2d.setColor(Color.RED);
// g2d.draw(new Line2D.Double(drawTo, pointFrom));
//
// double from = angleBetween(pointFrom, drawTo);
// System.out.println(NumberFormat.getNumberInstance().format(Math.toDegrees(from)));
//
// Point2D poc = getPointOnCircle(circle1, from);
// g2d.setColor(Color.BLUE);
// g2d.draw(new Line2D.Double(poc, drawTo));
//}
double from = angleBetween(circle1, circle2);
double to = angleBetween(circle2, circle1);
Point2D pointFrom = getPointOnCircle(circle1, from);
Point2D pointTo = getPointOnCircle(circle2, to);
g2d.setColor(Color.RED);
Line2D line = new Line2D.Double(pointFrom, pointTo);
g2d.draw(line);
g2d.dispose();
}
}
}
Arrow head
The intention is to treat the arrow head as a separate entity. The reason is because it's just simpler that way, you also get a more consistent result regardless of the distance between the objects.
So, to start with, I define a new Shape...
public class ArrowHead extends Path2D.Double {
public ArrowHead() {
int size = 10;
moveTo(0, size);
lineTo(size / 2, 0);
lineTo(size, size);
}
}
Pretty simple really. It just creates two lines, which point up, meeting in the middle of the available space.
Then in the paintComponent method, we perform some AffineTransform magic using the available information we already have, namely
The point on our target circles circumference
The angle to our target circle
And transform the ArrowHead shape...
g2d.setColor(Color.MAGENTA);
ArrowHead arrowHead = new ArrowHead();
AffineTransform at = AffineTransform.getTranslateInstance(
pointTo.getX() - (arrowHead.getBounds2D().getWidth() / 2d),
pointTo.getY());
at.rotate(from, arrowHead.getBounds2D().getCenterX(), 0);
arrowHead.transform(at);
g2d.draw(arrowHead);
Now, because I'm crazy, I also tested the code by drawing an arrow pointing at our source circle, just to prove that the calculations would work...
// This just proofs that the previous calculations weren't a fluke
// and that the arrow can be painted pointing to the source object as well
g2d.setColor(Color.GREEN);
arrowHead = new ArrowHead();
at = AffineTransform.getTranslateInstance(
pointFrom.getX() - (arrowHead.getBounds2D().getWidth() / 2d),
pointFrom.getY());
at.rotate(to, arrowHead.getBounds2D().getCenterX(), 0);
arrowHead.transform(at);
g2d.draw(arrowHead);
Let the first circle center coordinates are AX, AY, radius AR, and BX, BY, BR for the second circle.
Difference vector
D = (DX, DY) = (BX - AX, BY - AY)
Normalized
d = (dx, dy) = (DX / Length(D), DY / Length(D))
Start point of arrow
S = (sx, sy) = (AX + dx * AR, AY + dy * AR)
End point
E = (ex, ey) = (BX - dx * BR, BY - dy * BR)
Example:
AX = 0 AY = 0 AR = 1
BX = 4 BY = 3 BR = 2
D = (4, 3)
Length(D) = 5
dx = 4/5
dy = 3/5
sx = 0.8 sy = 0.6
ex = 4 - 2 * 4/5 = 12/5 = 2.4
ey = 3 - 2 * 3/5 = 9/5 = 1.8
Looking at the Screenshot, I think you need to find the top right corner of circle A, and then add half of the total distance to the bottom to y. Next, find the top right corner of circle B, and add half of the distance to the top left corner to x. Finally, make a line connecting the two, and render an arrow on the end of it.
Like this:
private int x1, y1, x2, y2 width = 20, height = 20;
private void example(Graphics g) {
// Set x1, x2, y1, and y2 to something
g.drawOval(x1, y1, width, height);
g.drawOval(x2, y2, width, height);
g.drawLine(x1, y1 + (height/2), x2 + (width/2), y2);
g.drawImage(/*Image of an arrow*/, (x2 + width/2)-2, y2);
}
My trick:
Let the two centers be C0 and C1. Using complex numbers, you map these two points to a horizontal segment from the origin by the transformation
P' = (P - C0) (C1 - C0)* / L
where * denotes conjugation and L = |C1 - C0|. (If you don't like the complex number notation, you can express this with matrices as well.)
Now the visible part of the segment goes from (R0, 0) to (L - R1, 0). The two other vertices of the arrow are at (L - R1 - H, W) and (L - R1 - H, -W) for an arrowhead of height H and width 2W.
By applying the inverse transform you get the original coordinates,
P = C0 + L P' / (C1 - C0)*.
I have a set of two dimensions points. Their X and Y are greater than -2 and lesser than 2. Such point could be : (-0.00012 ; 1.2334 ).
I would want to display these points on a graph, using rectangles (a rectangle illustrates a point, and has its coordinates set to its point's ones - moreover, it has a size of 10*10).
Rectangles like (... ; Y) should be displayed above any rectangles like (... ; Y-1) (positive Y direction is up). Thus, I must set the graph's origin not at the top-left hand-corner, but somewhere else.
I'm trying to use Graphics2D's AffineTransform to do that.
I get the minimal value for all the X coordinates
I get the minimal value for all the Y coordinates
I get the maximal value for all the X coordinates
I get the maximal value for all the Y coordinates
I get the distance xmax-xmin and ymax-ymin
Then, I wrote the code I give you below.
Screenshots
Some days ago, using my own method to scale, I had this graph:
(so as I explained, Y are inverted and that's not a good thing)
For the moment, i.e., with the code I give you below, I have only one point that takes all the graph's place! Not good at all.
I would want to have:
(without lines, and without graph's axis. The important here is that points are correctly displayed, according to their coordinates).
Code
To get min and max coordinates value:
x_min = Double.parseDouble((String) list_all_points.get(0).get(0));
x_max = Double.parseDouble((String) list_all_points.get(0).get(0));
y_min = Double.parseDouble((String) list_all_points.get(0).get(1));
y_max = Double.parseDouble((String) list_all_points.get(0).get(1));
for(StorableData s : list_all_points) {
if(Double.parseDouble((String) s.get(0)) < x_min) {
x_min = Double.parseDouble((String) s.get(0));
}
if(Double.parseDouble((String) s.get(0)) > x_max) {
x_max = Double.parseDouble((String) s.get(0));
}
if(Double.parseDouble((String) s.get(1)) < y_min) {
y_min = Double.parseDouble((String) s.get(1));
}
if(Double.parseDouble((String) s.get(1)) > y_max) {
y_max = Double.parseDouble((String) s.get(1));
}
}
To draw a point:
int x, y;
private void drawPoint(Cupple storable_data) {
//x = (int) (storable_data.getNumber(0) * scaling_coef + move_x);
//y = (int) (storable_data.getNumber(1) * scaling_coef + move_y);
x = storable_data.getNumber(0).intValue();
y = storable_data.getNumber(1).intValue();
graphics.fillRect(x, y, 10, 10);
graphics.drawString(storable_data.toString(), x - 5, y - 5);
}
To paint the graph:
#Override
public void paint(Graphics graphics) {
this.graphics = graphics;
Graphics2D graphics_2d = ((Graphics2D) this.graphics);
AffineTransform affine_transform = graphics_2d.getTransform();
affine_transform.scale(getWidth()/(x_max - x_min), getHeight()/(y_max - y_min));
affine_transform.translate(x_min, y_min);
graphics_2d.transform(affine_transform);
for(StorableData storable_data : list_all_points) {
graphics_2d.setColor(Color.WHITE);
this.drawPoint((Cupple) storable_data);
}
I suggest you map each data point to a point on the screen, thus avoiding the following coordinate system pitfalls. Take your list of points and create from them a list of points to draw. Take into account that:
The drawing is pixel-based, so you will want to scale your points (or you would have rectangles 1 to 4 pixels wide...).
You will need to translate all your points because negative values will be outside the boundaries of the component on which you draw.
The direction of the y axis is reversed in the drawing coordinates.
Once that is done, use the new list of points for the drawing and the initial one for calculations. Here is an example:
public class Graph extends JPanel {
private static int gridSize = 6;
private static int scale = 100;
private static int size = gridSize * scale;
private static int translate = size / 2;
private static int pointSize = 10;
List<Point> dataPoints, scaledPoints;
Graph() {
setBackground(Color.WHITE);
// points taken from your example
Point p1 = new Point(-1, -2);
Point p2 = new Point(-1, 0);
Point p3 = new Point(1, 0);
Point p4 = new Point(1, -2);
dataPoints = Arrays.asList(p1, p2, p3, p4);
scaledPoints = dataPoints.stream()
.map(p -> new Point(p.x * scale + translate, -p.y * scale + translate))
.collect(Collectors.toList());
}
#Override
public Dimension getPreferredSize() {
return new Dimension(size, size);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
// draw a grid
for (int i = 0; i < gridSize; i++) {
g2d.drawLine(i * scale, 0, i * scale, size);
g2d.drawLine(0, i * scale, size, i * scale);
}
// draw the rectangle
g2d.setPaint(Color.RED);
g2d.drawPolygon(scaledPoints.stream().mapToInt(p -> p.x).toArray(),
scaledPoints.stream().mapToInt(p -> p.y).toArray(),
scaledPoints.size());
// draw the points
g2d.setPaint(Color.BLUE);
// origin
g2d.fillRect(translate, translate, pointSize, pointSize);
g2d.drawString("(0, 0)", translate, translate);
// data
for (int i = 0; i < dataPoints.size(); i++) {
Point sp = scaledPoints.get(i);
Point dp = dataPoints.get(i);
g2d.fillRect(sp.x, sp.y, pointSize, pointSize);
g2d.drawString("(" + dp.x + ", " + dp.y + ")", sp.x, sp.y);
}
}
public static void main(String[] args) {
JFrame frame = new JFrame();
frame.setContentPane(new Graph());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
And another:
You might want to have the points aligned on the grid intersections and not below and to the right of them. I trust you will figure this one out.
Also, I ordered the points so that drawPolygon will paint the lines in the correct order. If your points are arbitrarily arranged, look for ways to find the outline. If you want lines between all points like in your example, iterate over all combinations of them with drawLine.
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.)
I need to:
1.) move the origin and also rotate the coordinate plane so that x-values progress rightward and y-values progress upward from the new origin(which needs to be the bottom left corner of the inner, blue rectangle in the code below). This will enable me to plot points at x,y coordinate pairs in the code below.
2.) plot rotated labels for the tic marks on the y-axis of the data plot.
The code below sets up this problem. It works, except for two problems:
1.) the data points are being plotted with the upper left hand corner as the origin and y-values descending downward
2.) the labels for the tic marks on the y-axis are not being drawn on the screen
Can anyone show me how to fix the code below so that it fixes these two problems and does what the first paragraph above describes?
The code is in the following two java files:
DataGUI.java
import java.awt.*;
import java.util.ArrayList;
import javax.swing.*;
class DataGUI extends JFrame{
DataGUI() {
super("X,Y Plot");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setPreferredSize(new Dimension(800, 400));
this.pack();
this.setSize(new Dimension(800, 600));
this.setLocationRelativeTo(null);
setLayout(new GridLayout());
ArrayList<Double> myDiffs = new ArrayList<Double>();
myDiffs.add(25.0);
myDiffs.add(9.0);
myDiffs.add(7.0);
myDiffs.add(16.0);
myDiffs.add(15.0);
myDiffs.add(6.0);
myDiffs.add(2.0);
myDiffs.add(8.0);
myDiffs.add(2.0);
myDiffs.add(27.0);
myDiffs.add(14.0);
myDiffs.add(12.0);
myDiffs.add(19.0);
myDiffs.add(10.0);
myDiffs.add(11.0);
myDiffs.add(8.0);
myDiffs.add(19.0);
myDiffs.add(2.0);
myDiffs.add(16.0);
myDiffs.add(5.0);
myDiffs.add(18.0);
myDiffs.add(23.0);
myDiffs.add(9.0);
myDiffs.add(4.0);
myDiffs.add(8.0);
myDiffs.add(9.0);
myDiffs.add(3.0);
myDiffs.add(3.0);
myDiffs.add(9.0);
myDiffs.add(13.0);
myDiffs.add(17.0);
myDiffs.add(7.0);
myDiffs.add(0.0);
myDiffs.add(2.0);
myDiffs.add(3.0);
myDiffs.add(33.0);
myDiffs.add(23.0);
myDiffs.add(26.0);
myDiffs.add(12.0);
myDiffs.add(12.0);
myDiffs.add(19.0);
myDiffs.add(14.0);
myDiffs.add(9.0);
myDiffs.add(26.0);
myDiffs.add(24.0);
myDiffs.add(13.0);
myDiffs.add(19.0);
myDiffs.add(2.0);
myDiffs.add(7.0);
myDiffs.add(28.0);
myDiffs.add(15.0);
myDiffs.add(2.0);
myDiffs.add(5.0);
myDiffs.add(17.0);
myDiffs.add(2.0);
myDiffs.add(16.0);
myDiffs.add(19.0);
myDiffs.add(2.0);
myDiffs.add(31.0);
DataPanel myPP = new DataPanel(myDiffs,this.getHeight(),this.getWidth());
this.add(myPP);
this.setVisible(true);// Display the panel.
}
public static void main(String[] args){
DataGUI myDataGUI = new DataGUI();
myDataGUI.setVisible(true);
}
}
DataPanel.java (Note: I edited the code below to include trashgod's suggestions, but it still does not work.)
import java.awt.*;
import java.awt.geom.AffineTransform;
import javax.swing.*;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.*;
class DataPanel extends JPanel {
Insets ins; // holds the panel's insets
ArrayList<Double> myDiffs;
double maxDiff = Double.NEGATIVE_INFINITY;
double minDiff = Double.POSITIVE_INFINITY;
double maxPlot;
DataPanel(ArrayList<Double> Diffs, int h, int w){
setOpaque(true);// Ensure that panel is opaque.
setPreferredSize(new Dimension(w, h));
setMinimumSize(new Dimension(w, h));
setMaximumSize(new Dimension(w, h));
myDiffs = Diffs;
repaint();
this.setVisible(true);
}
protected void paintComponent(Graphics g){// Override paintComponent() method.
super.paintComponent(g);
//get data about plotting environment and about text
int height = getHeight();
int width = getWidth();
ins = getInsets();
Graphics2D g2d = (Graphics2D)g;
FontMetrics fontMetrics = g2d.getFontMetrics();
String xString = ("x-axis label");
int xStrWidth = fontMetrics.stringWidth(xString);
int xStrHeight = fontMetrics.getHeight();
String yString = "y-axis label";
int yStrWidth = fontMetrics.stringWidth(yString);
int yStrHeight = fontMetrics.getHeight();
String titleString ="Title of Graphic";
int titleStrWidth = fontMetrics.stringWidth(titleString);
int titleStrHeight = fontMetrics.getHeight();
int leftMargin = ins.left;
//set parameters for inner rectangle
int hPad=10;
int vPad = 6;
int testLeftStartPlotWindow = ins.left+5+(3*yStrHeight);
int testInnerWidth = width-testLeftStartPlotWindow-ins.right-hPad;
getMaxMinDiffs();
getMaxPlotVal();
double increment = 5.0;
int numTicks = (int)(maxPlot/increment);//will use numTicks for: remainder, leftStartPlotWindow, innerRectangle+labels+tickmarks
int remainder = testInnerWidth%numTicks;
int leftStartPlotWindow = testLeftStartPlotWindow-remainder;
System.out.println("remainder is: "+remainder);
int bottomPad = (3*xStrHeight)-vPad;
int blueTop = ins.bottom+(vPad/2)+titleStrHeight;
int blueHeight = height-bottomPad-blueTop;
int blueWidth = blueHeight;
int blueBottom = blueHeight+blueTop;
//plot outer rectangle
g.setColor(Color.red);
int redWidth = width-leftMargin-1;
g.drawRect(leftMargin, ins.bottom, redWidth, height-ins.bottom-1);
//write top label
g.setColor(Color.black);
g.drawString(titleString, leftStartPlotWindow+((blueWidth/2)-(titleStrWidth/2)), titleStrHeight);
// fill, then plot, inner rectangle
g.setColor(Color.white);
g.fillRect(leftStartPlotWindow, blueTop, blueWidth, blueHeight);
g.setColor(Color.blue);
g.drawRect(leftStartPlotWindow, blueTop, blueWidth, blueHeight);
//scale the diffs to fit window
double Scalar = blueWidth/maxPlot;
ArrayList<Double> scaledDiffs = new ArrayList<Double>();
for(int e = 0;e<myDiffs.size();e++){scaledDiffs.add(myDiffs.get(e)*Scalar);}
//plot the scaled Diffs
AffineTransform at = g2d.getTransform();//save the graphics context's transform
g2d.translate(leftStartPlotWindow, blueTop);//translate origin to bottom-left corner of blue rectangle
g2d.scale(1, -1);//invert the y-axis
for(int w = 0;w<scaledDiffs.size();w++){
if(w>0){
double prior = scaledDiffs.get(w-1);
int priorInt = (int)prior;
double current = scaledDiffs.get(w);
int currentInt = (int)current;
g2d.drawOval(priorInt, currentInt, 4, 4);
}
}
g2d.setTransform(at);//restore the transform for conventional rendering
//write x-axis label
g.setColor(Color.red);
g.drawString(xString, leftStartPlotWindow+((blueWidth/2)-(xStrWidth/2)), height-ins.bottom-vPad);
//write y-axis label
g2d.rotate(Math.toRadians(-90), 0, 0);//rotate text 90 degrees counter-clockwise
g.drawString(yString, -(height/2)-(yStrWidth/2), yStrHeight);
g2d.rotate(Math.toRadians(+90), 0, 0);//rotate text 90 degrees clockwise
// draw tick marks on x-axis
NumberFormat formatter = new DecimalFormat("#0.0");
double k = (double)blueWidth/(double)numTicks;
double iteration = 0;
for(int h=0;h<=numTicks;h++){
int xval = (int)(h*k);
g.setColor(Color.red);
g.drawLine(leftStartPlotWindow+xval, blueBottom+2, leftStartPlotWindow+xval, blueBottom+(xStrHeight/2));//draw tick marks
g.drawString(formatter.format(iteration),leftStartPlotWindow+xval-(fontMetrics.stringWidth(Double.toString(iteration))/2),blueBottom+(xStrHeight/2)+13);
iteration+=increment;
}
// draw tick marks on y-axis
iteration = 0;
for(int h=0;h<=numTicks;h++){
int yval = (int)(h*k);
g.setColor(Color.red);
g.drawLine(leftStartPlotWindow-2, blueBottom-yval, leftStartPlotWindow-(yStrHeight/2), blueBottom-yval);//draw tick marks
g2d.rotate(Math.toRadians(-90), 0, 0);//rotate text 90 degrees counter-clockwise
g.drawString(formatter.format(iteration),leftStartPlotWindow-2,blueBottom-(fontMetrics.stringWidth(Double.toString(iteration))/2));
g2d.rotate(Math.toRadians(+90), 0, 0);//rotate text 90 degrees clockwise
iteration+=increment;
}
}
void getMaxMinDiffs(){// get max and min of Diffs
for(int u = 0;u<myDiffs.size();u++){
if(myDiffs.get(u)>maxDiff){maxDiff = myDiffs.get(u);}
if(myDiffs.get(u)<minDiff){minDiff = myDiffs.get(u);}
}
}
void getMaxPlotVal(){
maxPlot = maxDiff;
maxPlot += 1;//make sure maxPlot is bigger than the max data value
while(maxPlot%5!=0){maxPlot+=1;}//make sure maxPlot is a multiple of 5
}
}
Also, as always, links to articles or tutorials on the topic are much appreciated.
One approach is shown in SineTest. In outline,
Save the graphics context's transform.
Graphics2D g2d = (Graphics2D) g;
AffineTransform at = g2d.getTransform();
Translate the origin to the center.
g2d.translate(w / 2, h / 2);
Invert the y-axis.
g2d.scale(1, -1);
Render using cartesian coordinates.
Restore the transform for conventional rendering.
g2d.setTransform(at);
Apologies for somewhat incomplete answer, but this may get your gears turning. Java draws things the way you described them: It considers the top left corner of the screen to be 0, 0 and draws x increasing to the right and y increasing downwards. If you make the line that states
g2d.drawOval(priorInt, currentInt, 4, 4);
into
g2d.drawOval(blueWidth - priorInt, blueHeight - currentInt, 4, 4);
it should yield the correct results for your first issue. I need a bit more info on the second problem to help you with that one though. Are they just off the screen or are the getting drawn over by something else? Try flipping +s and -s around to see if you can get the correct result if that is the case.
I'm working on a 3D space trading game with some people, and one of the things I've been assigned to do is to make a guidance computer 'tunnel' that the ship travels through, with the tunnel made of squares that the user flies through to their destination, increasing in number as the user gets closer to the destination.
It's only necessary to render the squares for the points ahead of the ship, since that's all that's visible to the user. On their way to a destination, the ship's computer is supposed to put up squares on the HUD that represent fixed points in space between you and the destination, which are small in the distance and get larger as the points approach the craft.
I've had a go at implementing this and can't seem to figure it out, mainly using logarithms (Math.log10(x) and such). I tried to get to get the ship position in 'logarithmic space' to help find out what index to start from when drawing the squares, but then the fact that I only have distance to the destination to work with confuses the matter, especially when you consider that the number of squares has to vary dynamically to make sure they stay fixed at the right locations in space (i.e., the squares are positioned at intervals of 200 or so before being transformed logarithmically).
With regard to this, I had a working implementation with the ship between a start of 0.0d and end of 1.0d, although the implementation wasn't so nice. Anyway, the problem essentially boils down to a 1d nature. Any advice would be appreciated with this issue, including possible workarounds to achieve the same effect or solutions.
(Also, there's a Youtube video showing this effect: http://www.youtube.com/watch?v=79F9Nj7GgfM&t=3m5s)
Cheers,
Chris
Edit: rephrased the entire question.
Edit: new testbed code:
package st;
import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferStrategy;
import java.text.DecimalFormat;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class StUI2 extends JFrame {
public static final double DEG_TO_RAD = Math.PI / 180.0d;
public static final DecimalFormat decimalFormat = new DecimalFormat("0.0000");
public static final Font MONO = new Font("Monospaced", Font.PLAIN, 10);
public class StPanel extends Canvas {
protected final Object imgLock = new Object();
protected int lastWidth = 1, lastHeight = 1;
protected boolean first = true;
protected Color bgColour = Color.DARK_GRAY, gridColour = Color.GRAY;
double shipWrap = 700;
double shipFrame = 100;
double shipPos = 0;
long lastUpdateTimeMS = -1;
long currUpdateTimeMS = -1;
public StPanel() {
setFocusable(true);
setMinimumSize(new Dimension(1, 1));
setAlwaysOnTop(true);
}
public void internalPaint(Graphics2D g) {
synchronized (imgLock) {
if (lastUpdateTimeMS < 0) {
lastUpdateTimeMS = System.currentTimeMillis();
}
currUpdateTimeMS = System.currentTimeMillis();
long diffMS = currUpdateTimeMS - lastUpdateTimeMS;
g.setFont(MONO);
shipPos += (60d * ((double)diffMS / 1000));
if (shipPos > shipWrap) {
shipPos = 0d;
}
double shipPosPerc = shipPos / shipWrap;
double distToDest = shipWrap - shipPos;
double compression = 1000d / distToDest;
g.setColor(bgColour);
Dimension d = getSize();
g.fillRect(0, 0, (int)d.getWidth(), (int)d.getHeight());
//int amnt2 = (int)unlog10((1000d / distToDest));
g.setColor(Color.WHITE);
g.drawString("shipPos: " + decimalFormat.format(shipPos), 10, 10);
g.drawString("distToDest: " + decimalFormat.format(distToDest), 10, 20);
g.drawString("shipWrap: " + decimalFormat.format(shipWrap), 150, 10);
int offset = 40;
g.setFont(MONO);
double scalingFactor = 10d;
double dist = 0;
int curri = 0;
int i = 0;
do {
curri = i;
g.setColor(Color.GREEN);
dist = distToDest - getSquareDistance(distToDest, scalingFactor, i);
double sqh = getSquareHeight(dist, 100d * DEG_TO_RAD);
g.drawLine(30 + (int)dist, (offset + 50) - (int)(sqh / 2d), 30 + (int)dist, (offset + 50) + (int)(sqh / 2d));
g.setColor(Color.LIGHT_GRAY);
g.drawString("i: " + i + ", dist: " + decimalFormat.format(dist), 10, 120 + (i * 10));
i++;
} while (dist < distToDest);
g.drawLine(10, 122, 200, 122);
g.drawString("last / i: " + curri + ", dist: " + decimalFormat.format(dist), 10, 122 + (i * 10));
g.setColor(Color.MAGENTA);
g.fillOval(30 + (int)shipPos, offset + 50, 4, 4);
lastUpdateTimeMS = currUpdateTimeMS;
}
}
public double getSquareDistance(double initialDist, double scalingFactor, int num) {
return Math.pow(scalingFactor, num) * num * initialDist;
}
public double getSquareHeight(double distance, double angle) {
return distance / Math.tan(angle);
}
/* (non-Javadoc)
* #see java.awt.Canvas#paint(java.awt.Graphics)
*/
#Override
public void paint(Graphics g) {
internalPaint((Graphics2D)g);
}
public void redraw() {
synchronized (imgLock) {
Dimension d = getSize();
if (d.width == 0) d.width = 1;
if (d.height == 0) d.height = 1;
if (first || d.getWidth() != lastWidth || d.getHeight() != lastHeight) {
first = false;
// remake buf
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
//create an object that represents the device that outputs to screen (video card).
GraphicsDevice gd = ge.getDefaultScreenDevice();
gd.getDefaultConfiguration();
createBufferStrategy(2);
lastWidth = (int)d.getWidth();
lastHeight = (int)d.getHeight();
}
BufferStrategy strategy = getBufferStrategy();
Graphics2D g = (Graphics2D)strategy.getDrawGraphics();
internalPaint(g);
g.dispose();
if (!strategy.contentsLost()) strategy.show();
}
}
}
protected final StPanel canvas;
protected Timer viewTimer = new Timer(1000 / 60, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
canvas.redraw();
}
});
{
viewTimer.setRepeats(true);
viewTimer.setCoalesce(true);
}
/**
* Create the applet.
*/
public StUI2() {
JPanel panel = new JPanel(new BorderLayout());
setContentPane(panel);
panel.add(canvas = new StPanel(), BorderLayout.CENTER);
setVisible(true);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(800, 300);
setTitle("Targetting indicator test #2");
viewTimer.start();
}
public static double unlog10(double x) {
return Math.pow(10d, x);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
StUI2 ui = new StUI2();
}
});
}
}
Assuming you want the squares to be equal height (when you reach them), you can calculate a scaling factor based on the distance to the destination (d) and the required height of the squares upon reaching them (h).
From these two pieces of information you can calculate the inverse tangent (atan) of the angle (alpha) between the line connecting the ship to the destination (horizontal line in your image) and the line connecting the top of the squares with the destination (angled line in your image).
EDIT: corrected formula
Using the angle, you can calculate the height of the square (h') at any given distance from the destination: you know the distance to the destination (d') and the angle (alpha); The height of the square at distance d' is h'=r'*sin(alpha) -- sin(alpha)=cos(alpha)*tan(alpha) and r'=d'/cos(alpha) (the distance between the destination and the top of the square -- the "radius"). Or more easily: h'=d'*tan(alpha).
Note: adopting the algorithm to varying height (when you reach them) squares is relatively simple: when calculating the angle, just assume a (phantom) square of fixed height and scale the squares relatively to that.
If the height of the square at distance d' is calculated for you by your graphic library, all the better, you only need to figure out the distances to place the squares.
What distances to place the squares from the destination?
1) If you want a varying number of squares shown (in front of the ship), but potentially infinite number of squares to consider (based on d), you can chose the distance of the closest square to the destination (d1) and calculate the distances of other squares by the formula s^k*k*d1, where s (scaling factor) is a number > 1 for the k'th square (counting from the destination). You can stop the algorithm when the result is larger than d.
Note that if d is sufficiently large, the squares closest to the distance will block the destination (there are many of them and their heights are small due to the low angle). In this case you can introduce a minimal distance (possibly based on d), below which you do not display the squares -- you will have to experiment with the exact values to see what looks right/acceptable.
2) If you want a fixed amount of squares (sn) showing always, regardless of d, you can calculate the distances of the squares from the destination by the formula d*s^k, where s is a number < 1, k is the index of the square (counting from the ship). The consideration about small squares probably don't apply here unless sn is high.
To fix the updated code, change the relavant part to:
double dist = 0;
double d1 = 10;
int curri = 0;
int i = 1;
int maxSquareHeight = 40;
double angle = Math.atan(maxSquareHeight/distToDest);
while (true)
{
curri = i;
g.setColor(Color.GREEN);
dist = getSquareDistance(d1, scalingFactor, i);
if (dist > distToDest) {
break;
}
double sqh = getSquareHeight(dist, angle);
g.drawLine(30 + (int)(shipWrap - dist), offset+50-(int)(sqh / 2d), 30 + (int)(shipWrap - dist), offset+50+(int)(sqh / 2d));
g.setColor(Color.LIGHT_GRAY);
i++;
}
public double getSquareHeight(double distance, double angle) {
return distance * Math.tan(angle);
}
You should also reduce scalingFactor to the magnitude of ~1.5.
EDIT: If you replace the formula s^k*k*d1 with s^(k-1)*k*d1, then the first square will be exactly at distance d1.
EDIT: fixed square height calculating formula
EDIT: updated code