I'm trying to figure out why the setPitch in the PointPlacemarkAttributes does not seem to work correctly.
I believe this JOGL code in PointPlacemark.java is where things are going wrong:
Double heading = getActiveAttributes().getHeading();
Double pitch = getActiveAttributes().getPitch();
// Adjust heading to be relative to globe or screen
if (heading != null)
{
if (AVKey.RELATIVE_TO_GLOBE.equals(this.getActiveAttributes().getHeadingReference()))
heading = dc.getView().getHeading().degrees - heading;
else
heading = -heading;
}
// Apply the heading and pitch if specified.
if (heading != null || pitch != null)
{
gl.glTranslated(xscale / 2, yscale / 2, 0);
if (pitch != null)
gl.glRotated(pitch, 1, 0, 0);
if (heading != null)
gl.glRotated(heading, 0, 0, 1);
gl.glTranslated(-xscale / 2, -yscale / 2, 0);
}
// Scale the unit quad
gl.glScaled(xscale, yscale, 1);
Here is a simple driver I've been using to play with it:
public class Placemarks extends ApplicationTemplate {
public static class AppFrame extends ApplicationTemplate.AppFrame {
public AppFrame() {
super(true, true, false);
final RenderableLayer layer = new RenderableLayer();
PointPlacemark pp = new PointPlacemark(Position.fromDegrees(28, -102, 30000));
pp.setLabelText("PointPlacemark");
pp.setLineEnabled(false);
pp.setAltitudeMode(WorldWind.ABSOLUTE);
PointPlacemarkAttributes attrs = new PointPlacemarkAttributes();
attrs.setImageAddress("gov/nasa/worldwindx/examples/images/georss.png");
attrs.setScale(1.0);
attrs.setImageOffset(Offset.CENTER);
attrs.setPitch(45.0);
pp.setAttributes(attrs);
layer.addRenderable(pp);
// Add the layer to the model.
insertBeforeCompass(getWwd(), layer);
}
}
public static void main(String[] args) {
ApplicationTemplate.start("WorldWind Placemarks", AppFrame.class);
}
}
If I set no pitch, it looks fine:
But when I set a pitch of 45 degrees it looks like this:
Which I'm not understanding how it correlates to the value I set. I'd expect it to work like the Compass does in the CompassLayer:
Update
Comment suggested to iterate through pitch values to see how it works. I did that and I'm still not seeing how it is supposed to work. It looks like it is just "cropping" the image horizontally, and not doing anything else. Here is some code:
public class Placemarks extends ApplicationTemplate {
public static class AppFrame extends ApplicationTemplate.AppFrame {
public AppFrame() {
super(true, true, false);
final RenderableLayer layer = new RenderableLayer();
PointPlacemark pp = new PointPlacemark(Position.fromDegrees(28, -102, 30000));
pp.setLabelText("PointPlacemark");
pp.setLineEnabled(false);
pp.setAltitudeMode(WorldWind.ABSOLUTE);
PointPlacemarkAttributes attrs = new PointPlacemarkAttributes();
attrs.setImageAddress("gov/nasa/worldwindx/examples/images/georss.png");
attrs.setScale(1.0);
attrs.setImageOffset(Offset.CENTER);
pp.setAttributes(attrs);
layer.addRenderable(pp);
// Add the layer to the model.
insertBeforeCompass(getWwd(), layer);
Thread t = new Thread(new Runnable() {
#Override
public void run() {
for(double i = 0.0; i<360; i+=.1) {
attrs.setPitch(i);
System.out.println("Pitch is now "+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
AppFrame.this.getWwd().redrawNow();
}
}
});
t.start();
}
}
public static void main(String[] args) {
ApplicationTemplate.start("WorldWind Placemarks", AppFrame.class);
}
}
And a screen recorded GIF:
The problem is that in PointPlacemark.doDrawOrderedRenderable(), the orthographic projection matrix used uses a range of depth values from -1 to 1.
When the pitch remains at 0, the z coordinates also remain at 0, safely in the middle of this range (actually, there is some slight fudging of this coordinate in WorldWind, but never mind that). As it pitches, of course the z coordinates change, until at 90° all of the y coordinates are 0 while z will go to half of the height of the image. This is why only a slice of the image that falls within the range -1,1 is visible while the rest is clipped.
That z range is defined by the following code:
// The image is drawn using a parallel projection.
osh.pushProjectionIdentity(gl);
gl.glOrtho(0d, dc.getView().getViewport().width, 0d, dc.getView().getViewport().height, -1d, 1d);
If we examine the equivalent code in CompassLayer, we can see that here they do factor in the scaled icon size (although the comment suggests that perhaps at some earlier iteration, less care had been taken over the z dimension):
double width = this.getScaledIconWidth();
double height = this.getScaledIconHeight();
// Load a parallel projection with xy dimensions (viewportWidth, viewportHeight)
// into the GL projection matrix.
java.awt.Rectangle viewport = dc.getView().getViewport();
ogsh.pushProjectionIdentity(gl);
double maxwh = width > height ? width : height;
if (maxwh == 0)
maxwh = 1;
gl.glOrtho(0d, viewport.width, 0d, viewport.height, -0.6 * maxwh, 0.6 * maxwh);
In this case, the arguments for z (±0.6 * maxwh) use 0.6 presumably as 0.5 plus some margin. The actual geometry is a unit quad, which is translated by half width/height in x/y, scaled and rotated accordingly.
For PointPlacemark, we can account for the size of the renderable in a similar way. Rearranging the code slightly so that scale computation happens before setting the projection, and adding a maxwh value:
// Compute the scale
double xscale;
Double scale = this.getActiveAttributes().getScale();
if (scale != null)
xscale = scale * this.activeTexture.getWidth(dc);
else
xscale = this.activeTexture.getWidth(dc);
double yscale;
if (scale != null)
yscale = scale * this.activeTexture.getHeight(dc);
else
yscale = this.activeTexture.getHeight(dc);
double maxwh = Math.max(xscale, yscale);
// The image is drawn using a parallel projection.
osh.pushProjectionIdentity(gl);
gl.glOrtho(0d, dc.getView().getViewport().width, 0d, dc.getView().getViewport().height, -0.6 * maxwh, 0.6 * maxwh);
Again, 0.6 allows some margin.
It would probably be perfectly fine to have hardcoded values for the z range, as long as they were large enough for any image we might want to draw but not so large that numerical precision became an issue. Conversely, one could go even further and factor in trig to work out the actual depth needed for a given rotation and image size, but there would not be much to gain by doing so.
This was indeed a bug with WorldWindJava that has been reported, along with a link here for the fix.
Related
I'm currently recreating a Civilization game in Processing. I'm planning to implement the feature in a which a given unit can see every possible move it can make with a given number of hexes it is allowed to move. All possible endpoints are marked with red circles. However, units cannot move through mountains or bodies of water. I'm trying to approach this by finding out every possible combination of moves I can make without the unit going into a mountain or body of water but I can't figure out how I can determine every combination.
There are 6 directions that any unit can go in, north-east, north, north-west, south-east, south, south-west. The max number of movements I'm assigning to any unit would probably go up to 6. Any higher and I'm afraid processing may become to slow every time I move a unit.
I'm trying to recreate this:
What I'm hoping the result will look like with two possible movements (without the black arrows):
Raw version of that image:
Here is the code I use to draw the hex grid. Immediately after drawing each individual hex, its center's x coords and y coords are stored in xHexes and yHexes respectively. Also, immediately after generating the type of tile (e.g. grass, beach), the type of tile is also stored in an array named hexTypes. Therefore, I can get the x and y coords and type of hex of any hex I want on the map just by referencing its index.
Code used to draw a single hexagon:
beginShape();
for (float a = PI/6; a < TWO_PI; a += TWO_PI/6) {
float vx = x + cos(a) * gs*2;
float vy = y + sin(a) * gs*2;
vertex(vx, vy);
}
x is the x coord for centre of hexagon
y is the y coord for centre of hexagon
gs = radius of hexagon
Code used to tesselate hex over the window creating a hex grid:
void redrawMap() {
float xChange = 1.7;
float yChange = 6;
for (int y = 0; y < ySize/hexSize; y++) {
for (int x = 0; x < xSize/hexSize; x++) {
if (x % 2 == 1) {
// if any part of this hexagon being formed will be visible on the window and not off the window.
if (x*hexSize*xChange <= width+2*hexSize && int(y*hexSize*yChange) <= height+3*hexSize) {
drawHex(x*hexSize*xChange, y*hexSize*yChange, hexSize);
}
// only record and allow player to react with it if the entire tile is visible on the window
if (x*hexSize*xChange < width && int(y*hexSize*yChange) < height) {
xHexes.add(int(x*hexSize*xChange));
yHexes.add(int(y*hexSize*yChange));
}
} else {
if (x*hexSize*xChange <= width+2*hexSize && int(y*hexSize*yChange) <= height+3*hexSize) {
drawHex(x*hexSize*xChange, y*hexSize*yChange+(hexSize*3), hexSize);
}
if (x*hexSize*xChange < width && int(y*hexSize*yChange+(hexSize*3)) < height) {
xHexes.add(int(x*hexSize*xChange));
yHexes.add(int(y*hexSize*yChange+(hexSize*3)));
}
}
}
}
}
hexSize is a user-specified size for each hexagon, determining the number of hexagons that will be on the screen.
This answer will help you get to this (green is plains, red is hills and blue is water, also please don't flame my terrible grid):
Note that there is no pathfinding in this solution, only some very simple "can I get there" math. I'll include the full code of the sketch at the end so you can reproduce what I did and test it yourself. One last thing: this answer doesn't use any advanced design pattern, but it assume that you're confortable with the basics and Object Oriented Programming. If I did something which you're not sure you understand, you can (and should) ask about it.
Also: this is a proof of concept, not a "copy and paste me" solution. I don't have your code, so it cannot be that second thing anyway, but as your question can be solved in a bazillion manners, this is only one which I deliberately made as simple and visual as possible so you can get the idea and run with it.
First, I strongly suggest that you make your tiles into objects. First because they need to carry a lot of information (what's on each tile, how hard they are to cross, maybe things like resources or yield... I don't know, but there will be a lot of stuff).
The Basics
I organized my global variables like this:
// Debug
int unitTravelPoints = 30; // this is the number if "travel points" currently being tested, you can change it
// Golbals
float _tileSize = 60;
int _gridWidth = 10;
int _gridHeight = 20;
ArrayList<Tile> _tiles = new ArrayList<Tile>(); // all the tiles
ArrayList<Tile> _canTravel = new ArrayList<Tile>(); // tiles you can currently travel to
The basics being that I like to be able to change my grid size on the fly, but that's just a detail. What's next is to choose a coordinate system for the grid. I choose the simplest one as I didn't want to bust my brain on something complicated, but you may want to adapt this to another coordinate system. I choose the offset coordinate type of grid: my "every second row" is half a tile offset. So, instead of having this:
I have this:
The rest is just adjusting the spatial coordinates of the tiles so it doesn't look too bad, but their coordinates stays the same:
Notice how I consider that the spatial coordinates and the grid coordinates are two different things. I'll mostly use the spatial coordinates for the proximity checks, but that's because I'm lazy, because you could make a nice algorithm which do the same thing without the spatial coordinates and it would probably be less costly.
What about the travel points? Here's how I decided to work: your unit has a finite amount of "travel points". Here there's no unit, but instead a global variable unitTravelPoints which will do the same thing. I decided to work with this scale: one normal tile is worth 10 travel points. So:
Plains: 10 points
Hills: 15 points
Water: 1000 points (this is impassable terrain but without going into the details)
I'm not going to go into the details of drawing a grid, but that's mostly because your algorithm looks way better than mine on this front. On the other hand, I'll spend some time on explaining how I designed the Tiles.
The Tiles
We're entering OOP: they are Drawable. Drawable is a base class which contains some basic info which every visible thing should have: a position, and an isVisible setting which can be turned off. And a method to draw it, which I call Render() since draw() is already taken by Processing:
class Drawable {
PVector position;
boolean isVisible;
public Drawable() {
position = new PVector(0, 0);
isVisible = true;
}
public void Render() {
// If you forget to overshadow the Render() method you'll see this error message in your console
println("Error: A Drawable just defaulted to the catch-all Render(): '" + this.getClass() + "'.");
}
}
The Tile will be more sophisticated. It'll have more basic informations: row, column, is it currently selected (why not), a type like plains or hills or water, a bunch of neighboring tiles, a method to draw itself and a method to know if the unit can travel through it:
class Tile extends Drawable {
int row, column;
boolean selected = false;
TileType type;
ArrayList<Tile> neighbors = new ArrayList<Tile>();
Tile(int row, int column, TileType type) {
super(); // this calls the parent class' constructor
this.row = row;
this.column = column;
this.type = type;
// the hardcoded numbers are all cosmetics I included to make my grid looks less awful, nothing to see here
position.x = (_tileSize * 1.5) * (column + 1);
position.y = (_tileSize * 0.5) * (row + 1);
// this part checks if this is an offset row to adjust the spatial coordinates
if (row % 2 != 0) {
position.x += _tileSize * 0.75;
}
}
// this method looks recursive, but isn't. It doesn't call itself, but it calls it's twin from neighbors tiles
void FillCanTravelArrayList(int travelPoints, boolean originalTile) {
if (travelPoints >= type.travelCost) {
// if the unit has enough travel points, we add the tile to the "the unit can get there" list
if (!_canTravel.contains(this)) {
// well, only if it's not already in the list
_canTravel.add(this);
}
// then we check if the unit can go further
for (Tile t : neighbors) {
if (originalTile) {
t.FillCanTravelArrayList(travelPoints, false);
} else {
t.FillCanTravelArrayList(travelPoints - type.travelCost, false);
}
}
}
}
void Render() {
if (isVisible) {
// the type knows which colors to use, so we're letting the type draw the tile
type.Render(this);
}
}
}
The Tile Types
The TileType is a strange animal: it's a real class, but it's never used anywhere. That's because it's a common root for all tile types, which will inherit it's basics. The "City" tile may need very different variables than, say, the "Desert" tile. But both need to be able to draw themselves, and both need to be owned by the tiles.
class TileType {
// cosmetics
color fill = color(255, 255, 255);
color stroke = color(0);
float strokeWeight = 2;
// every tile has a "travelCost" variable, how much it cost to travel through it
int travelCost = 10;
// while I put this method here, it could have been contained in many other places
// I just though that it made sense here
void Render(Tile tile) {
fill(fill);
if (tile.selected) {
stroke(255);
} else {
stroke(stroke);
}
strokeWeight(strokeWeight);
DrawPolygon(tile.position.x, tile.position.y, _tileSize/2, 6);
textAlign(CENTER, CENTER);
fill(255);
text(tile.column + ", " + tile.row, tile.position.x, tile.position.y);
}
}
Each tile type can be custom, now, yet each tile is... just a tile, whatever it's content. Here are the TileType I used in this demonstration:
// each different tile type will adjust details like it's travel cost or fill color
class Plains extends TileType {
Plains() {
this.fill = color(0, 125, 0);
this.travelCost = 10;
}
}
class Water extends TileType {
// here I'm adding a random variable just to show that you can custom those types with whatever you need
int numberOfFishes = 10;
Water() {
this.fill = color(0, 0, 125);
this.travelCost = 1000;
}
}
class Hill extends TileType {
Hill() {
this.fill = color(125, 50, 50);
this.travelCost = 15;
}
}
Non-class methods
I added a mouseClicked() method so we can select a hex to check how far from it the unit can travel. In your game, you would have to make it so when you select a unit all these things fall into place, but as this is just a proof of concept the unit is imaginary and it's location is wherever you click.
void mouseClicked() {
// clearing the array which contains tiles where the unit can travel as we're changing those
_canTravel.clear();
for (Tile t : _tiles) {
// select the tile we're clicking on (and nothing else)
t.selected = IsPointInRadius(t.position, new PVector(mouseX, mouseY), _tileSize/2);
if (t.selected) {
// if a tile is selected, check how far the imaginary unit can travel
t.FillCanTravelArrayList(unitTravelPoints, true);
}
}
}
At last, I added 2 "helper methods" to make things easier:
// checks if a point is inside a circle's radius
boolean IsPointInRadius(PVector center, PVector point, float radius) {
// simple math, but with a twist: I'm not using the square root because it's costly
// we don't need to know the distance between the center and the point, so there's nothing lost here
return pow(center.x - point.x, 2) + pow(center.y - point.y, 2) <= pow(radius, 2);
}
// draw a polygon (I'm using it to draw hexagons, but any regular shape could be drawn)
void DrawPolygon(float x, float y, float radius, int npoints) {
float angle = TWO_PI / npoints;
beginShape();
for (float a = 0; a < TWO_PI; a += angle) {
float sx = x + cos(a) * radius;
float sy = y + sin(a) * radius;
vertex(sx, sy);
}
endShape(CLOSE);
}
How Travel is calculated
Behind the scenes, that's how the program knows where the unit can travel: in this example, the unit has 30 travel points. Plains cost 10, hills cost 15. If the unit has enough points left, the tile is marked as "can travel there". Every time a tile is in travel distance, we also check if the unit can get further from this tile, too.
Now, if you're still following me, you may ask: how do the tiles know which other tile is their neighbor? That's a great question. I suppose that an algorithm checking their coordinates would be the best way to handle this, but as this operation will happen only once when we create the map I decided to take the easy route and check which tiles were the close enough spatially:
void setup() {
// create the grid
for (int i=0; i<_gridWidth; i++) {
for (int j=0; j<_gridHeight; j++) {
int rand = (int)random(100);
if (rand < 20) {
_tiles.add(new Tile(j, i, new Water()));
} else if (rand < 50) {
_tiles.add(new Tile(j, i, new Hill()));
} else {
_tiles.add(new Tile(j, i, new Plains()));
}
}
}
// detect and save neighbor tiles for every Tile
for (Tile currentTile : _tiles) {
for (Tile t : _tiles) {
if (t != currentTile) {
if (IsPointInRadius(currentTile.position, t.position, _tileSize)) {
currentTile.neighbors.add(t);
}
}
}
}
}
Complete code for copy-pasting
Here's the whole thing in one place so you can easily copy and paste it into a Processing IDE and play around with it (also, it includes how I draw my awful grid):
// Debug
int unitTravelPoints = 30; // this is the number if "travel points" currently being tested, you can change it
// Golbals
float _tileSize = 60;
int _gridWidth = 10;
int _gridHeight = 20;
ArrayList<Tile> _tiles = new ArrayList<Tile>();
ArrayList<Tile> _canTravel = new ArrayList<Tile>();
void settings() {
// this is how to make a window size's dynamic
size((int)(((_gridWidth+1) * 1.5) * _tileSize), (int)(((_gridHeight+1) * 0.5) * _tileSize));
}
void setup() {
// create the grid
for (int i=0; i<_gridWidth; i++) {
for (int j=0; j<_gridHeight; j++) {
int rand = (int)random(100);
if (rand < 20) {
_tiles.add(new Tile(j, i, new Water()));
} else if (rand < 50) {
_tiles.add(new Tile(j, i, new Hill()));
} else {
_tiles.add(new Tile(j, i, new Plains()));
}
}
}
// detect and save neighbor tiles for every Tile
for (Tile currentTile : _tiles) {
for (Tile t : _tiles) {
if (t != currentTile) {
if (IsPointInRadius(currentTile.position, t.position, _tileSize)) {
currentTile.neighbors.add(t);
}
}
}
}
}
void draw() {
background(0);
// show the tiles
for (Tile t : _tiles) {
t.Render();
}
// show how far you can go
for (Tile t : _canTravel) {
fill(0, 0, 0, 0);
if (t.selected) {
stroke(255);
} else {
stroke(0, 255, 0);
}
strokeWeight(5);
DrawPolygon(t.position.x, t.position.y, _tileSize/2, 6);
}
}
class Drawable {
PVector position;
boolean isVisible;
public Drawable() {
position = new PVector(0, 0);
isVisible = true;
}
public void Render() {
// If you forget to overshadow the Render() method you'll see this error message in your console
println("Error: A Drawable just defaulted to the catch-all Render(): '" + this.getClass() + "'.");
}
}
class Tile extends Drawable {
int row, column;
boolean selected = false;
TileType type;
ArrayList<Tile> neighbors = new ArrayList<Tile>();
Tile(int row, int column, TileType type) {
super(); // this calls the parent class' constructor
this.row = row;
this.column = column;
this.type = type;
// the hardcoded numbers are all cosmetics I included to make my grid looks less awful, nothing to see here
position.x = (_tileSize * 1.5) * (column + 1);
position.y = (_tileSize * 0.5) * (row + 1);
// this part checks if this is an offset row to adjust the spatial coordinates
if (row % 2 != 0) {
position.x += _tileSize * 0.75;
}
}
// this method looks recursive, but isn't. It doesn't call itself, but it calls it's twin from neighbors tiles
void FillCanTravelArrayList(int travelPoints, boolean originalTile) {
if (travelPoints >= type.travelCost) {
// if the unit has enough travel points, we add the tile to the "the unit can get there" list
if (!_canTravel.contains(this)) {
// well, only if it's not already in the list
_canTravel.add(this);
}
// then we check if the unit can go further
for (Tile t : neighbors) {
if (originalTile) {
t.FillCanTravelArrayList(travelPoints, false);
} else {
t.FillCanTravelArrayList(travelPoints - type.travelCost, false);
}
}
}
}
void Render() {
if (isVisible) {
// the type knows which colors to use, so we're letting the type draw the tile
type.Render(this);
}
}
}
class TileType {
// cosmetics
color fill = color(255, 255, 255);
color stroke = color(0);
float strokeWeight = 2;
// every tile has a "travelCost" variable, how much it cost to travel through it
int travelCost = 10;
// while I put this method here, it could have been contained in many other places
// I just though that it made sense here
void Render(Tile tile) {
fill(fill);
if (tile.selected) {
stroke(255);
} else {
stroke(stroke);
}
strokeWeight(strokeWeight);
DrawPolygon(tile.position.x, tile.position.y, _tileSize/2, 6);
textAlign(CENTER, CENTER);
fill(255);
text(tile.column + ", " + tile.row, tile.position.x, tile.position.y);
}
}
// each different tile type will adjust details like it's travel cost or fill color
class Plains extends TileType {
Plains() {
this.fill = color(0, 125, 0);
this.travelCost = 10;
}
}
class Water extends TileType {
// here I'm adding a random variable just to show that you can custom those types with whatever you need
int numberOfFishes = 10;
Water() {
this.fill = color(0, 0, 125);
this.travelCost = 1000;
}
}
class Hill extends TileType {
Hill() {
this.fill = color(125, 50, 50);
this.travelCost = 15;
}
}
void mouseClicked() {
// clearing the array which contains tiles where the unit can travel as we're changing those
_canTravel.clear();
for (Tile t : _tiles) {
// select the tile we're clicking on (and nothing else)
t.selected = IsPointInRadius(t.position, new PVector(mouseX, mouseY), _tileSize/2);
if (t.selected) {
// if a tile is selected, check how far the imaginary unit can travel
t.FillCanTravelArrayList(unitTravelPoints, true);
}
}
}
// checks if a point is inside a circle's radius
boolean IsPointInRadius(PVector center, PVector point, float radius) {
// simple math, but with a twist: I'm not using the square root because it's costly
// we don't need to know the distance between the center and the point, so there's nothing lost here
return pow(center.x - point.x, 2) + pow(center.y - point.y, 2) <= pow(radius, 2);
}
// draw a polygon (I'm using it to draw hexagons, but any regular shape could be drawn)
void DrawPolygon(float x, float y, float radius, int npoints) {
float angle = TWO_PI / npoints;
beginShape();
for (float a = 0; a < TWO_PI; a += angle) {
float sx = x + cos(a) * radius;
float sy = y + sin(a) * radius;
vertex(sx, sy);
}
endShape(CLOSE);
}
Hope it'll help. Have fun!
You will have to use similar algorithms we use on pathfinding. you create a stack or queue that will hold a class storing the position of the hex and the number of moves left from that point, initially you insert your starting position with the number of moves you have and mark that hex as done ( to not re-use a position you have already been on ), then you pop an element, and you insert every neighbor of that hex with a number of moves -1. when you insert the hexes with zero moves, those are your endpoints. And before inserting any hex check if it's not already done.
I hope I was clear, your question was a bit vague but I tried to give you an idea of how these solutions are usually done, also I think your question fits more into algorithms rather then processing
Best of luck
In NASA WorldWind Java, I'm using PointPlacemark to represent an image because it stays the same size regardless of zoom level. The problem is that I want to set the heading on the Point Placemark and have it stay on that compass heading even when the camera is tilted. It works exactly as I want when viewing an untilted globe, but when I tilt, the placemark continues to face the screen instead of tilting with the globe, which causes it to act strange.
Here is a GIF illustrating what I'm seeing: https://giphy.com/embed/3o7WIqZUceR8xh6BOg
I would like the Point Placemark Image to stay on a heading relative to the globe, even when tilted -- so the image essentially is "flattened" as the view is tilted, while still remaining the same size regardless of zoom level.
Here is a code snippet that I'm using. I am setting attrs.setHeadingReference(AVKey.RELATIVE_TO_GLOBE); on the associated PointPlacemarkAttributes. In this example, I am setting the heading to 135 degrees.
import gov.nasa.worldwind.WorldWind;
import gov.nasa.worldwind.avlist.AVKey;
import gov.nasa.worldwind.geom.Position;
import gov.nasa.worldwind.layers.RenderableLayer;
import gov.nasa.worldwind.render.Offset;
import gov.nasa.worldwind.render.PointPlacemark;
import gov.nasa.worldwind.render.PointPlacemarkAttributes;
public class Placemarks extends ApplicationTemplate {
public static class AppFrame extends ApplicationTemplate.AppFrame {
public AppFrame() {
super(true, true, false);
final RenderableLayer layer = new RenderableLayer();
PointPlacemark pp = new PointPlacemark(Position.fromDegrees(28, -102, 30000));
pp.setLabelText("Airplane");
pp.setLineEnabled(false);
pp.setAltitudeMode(WorldWind.ABSOLUTE);
PointPlacemarkAttributes attrs = new PointPlacemarkAttributes();
attrs.setImageAddress("images/airplane.png");
attrs.setScale(0.05);
attrs.setImageOffset(Offset.CENTER);
//Point to 135.0
attrs.setHeading(135.0);
attrs.setHeadingReference(AVKey.RELATIVE_TO_GLOBE);
pp.setAttributes(attrs);
layer.addRenderable(pp);
// Add the layer to the model.
insertBeforeCompass(getWwd(), layer);
}
}
public static void main(String[] args) {
ApplicationTemplate.start("WorldWind Placemarks", AppFrame.class);
}
}
I've also played with using a Polygon with a Texture applied to it. The way it is oriented is what I'm looking for -- except I want the icon to remain the same size regardless of zoom level (like what the PointPlacemark does).
Here is a GIF illustrating what I'm seeing when using a Polygon. Note how it acts when the globe is tilted: https://giphy.com/embed/xThta4USlDzd8Ii5ZS
Here is the source I'm using for the Polygon:
import java.awt.geom.AffineTransform;
import java.util.Arrays;
import java.util.List;
import gov.nasa.worldwind.WorldWind;
import gov.nasa.worldwind.geom.Position;
import gov.nasa.worldwind.layers.RenderableLayer;
import gov.nasa.worldwind.render.BasicShapeAttributes;
import gov.nasa.worldwind.render.Polygon;
public class TexturedPolygon extends ApplicationTemplate {
public static Polygon createPolygonTexturedImage(String filePath, Position pos, double heading, double scale) {
double offsetDist = 1.0D * scale;
Position p1 = Position.fromDegrees(pos.getLatitude().addDegrees(-offsetDist).getDegrees(),
pos.getLongitude().addDegrees(-offsetDist).getDegrees(), pos.getAltitude());
Position p2 = Position.fromDegrees(pos.getLatitude().addDegrees(offsetDist).getDegrees(),
pos.getLongitude().addDegrees(-offsetDist).getDegrees());
Position p3 = Position.fromDegrees(pos.getLatitude().addDegrees(offsetDist).getDegrees(),
pos.getLongitude().addDegrees(offsetDist).getDegrees());
Position p4 = Position.fromDegrees(pos.getLatitude().addDegrees(-offsetDist).getDegrees(),
pos.getLongitude().addDegrees(offsetDist).getDegrees());
double[] points = new double[] { p1.getLatitude().getDegrees(), p1.getLongitude().getDegrees(),
p2.getLatitude().getDegrees(), p2.getLongitude().getDegrees(), p3.getLatitude().getDegrees(),
p3.getLongitude().getDegrees(), p4.getLatitude().getDegrees(), p4.getLongitude().getDegrees() };
double[] transformedPoints = new double[8];
AffineTransform rotation = new AffineTransform();
rotation.rotate(Math.toRadians(heading), pos.getLatitude().getDegrees(), pos.getLongitude().getDegrees());
rotation.transform(points, 0, transformedPoints, 0, 4);
double altitude = pos.getAltitude();
p1 = Position.fromDegrees(transformedPoints[0], transformedPoints[1], altitude);
p2 = Position.fromDegrees(transformedPoints[2], transformedPoints[3], altitude);
p3 = Position.fromDegrees(transformedPoints[4], transformedPoints[5], altitude);
p4 = Position.fromDegrees(transformedPoints[6], transformedPoints[7], altitude);
List<Position> positions = Arrays.asList(p1, p2, p3, p4);
Polygon polygon = new Polygon(positions);
polygon.setAltitudeMode(WorldWind.ABSOLUTE);
BasicShapeAttributes mattr = new BasicShapeAttributes();
mattr.setDrawOutline(false);
mattr.setDrawInterior(true);
polygon.setAttributes(mattr);
polygon.setTextureImageSource(filePath, new float[] { 0.0F, 0.0F, 1.0F, 0.0F, 1.0F, 1.0F, 0.0F, 1.0F }, 4);
return polygon;
}
public static class AppFrame extends ApplicationTemplate.AppFrame {
public AppFrame() {
super(true, true, false);
final RenderableLayer layer = new RenderableLayer();
Position pos = Position.fromDegrees(28, -102, 30000);
String url = "images/airplane.png";
layer.addRenderable(createPolygonTexturedImage(url, pos, 135.0, 1.05));
// Add the layer to the model.
insertBeforeCompass(getWwd(), layer);
}
}
public static void main(String[] args) {
ApplicationTemplate.start("WorldWind Placemarks", AppFrame.class);
}
}
For completeness sake -- here is the image I'm using as my airplane.png:
So to sum it up, what I'm looking for:
A Renderable represented by an Icon Image
Icon stays the same size regardless of zoom level
Icon stays oriented at a globe compass heading, even when camera view tilted
By combining the solution to this question and the CompassLayer logic that ties the screen tilt to the pitch.
Add this method to PointPlacemark.java (Taken from CompassLayer):
protected double computePitch(View view)
{
if (view == null)
return 0.0;
if (!(view instanceof OrbitView))
return 0.0;
OrbitView orbitView = (OrbitView) view;
return orbitView.getPitch().getDegrees();
}
And then in the doDrawOrderedRenderable(DrawContext dc, PickSupport pickCandidates, OrderedPlacemark opm) method, use this logic:
protected void doDrawOrderedRenderable(DrawContext dc, PickSupport pickCandidates, OrderedPlacemark opm)
{
if (this.isDrawLine(dc, opm))
this.drawLine(dc, pickCandidates, opm);
if (this.activeTexture == null)
{
if (this.isDrawPoint(dc))
this.drawPoint(dc, pickCandidates, opm);
return;
}
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
OGLStackHandler osh = new OGLStackHandler();
try
{
if (dc.isPickingMode())
{
// Set up to replace the non-transparent texture colors with the single pick color.
gl.glEnable(GL.GL_TEXTURE_2D);
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_TEXTURE_ENV_MODE, GL2.GL_COMBINE);
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_SRC0_RGB, GL2.GL_PREVIOUS);
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_COMBINE_RGB, GL2.GL_REPLACE);
Color pickColor = dc.getUniquePickColor();
pickCandidates.addPickableObject(this.createPickedObject(dc, pickColor));
gl.glColor3ub((byte) pickColor.getRed(), (byte) pickColor.getGreen(), (byte) pickColor.getBlue());
}
else
{
gl.glEnable(GL.GL_TEXTURE_2D);
Color color = this.getActiveAttributes().getImageColor();
if (color == null)
color = PointPlacemarkAttributes.DEFAULT_IMAGE_COLOR;
gl.glColor4ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue(),
(byte) color.getAlpha());
}
// This was relocated from the check in version.
// Compute the scale
double xscale;
Double scale = this.getActiveAttributes().getScale();
if (scale != null)
xscale = scale * this.activeTexture.getWidth(dc);
else
xscale = this.activeTexture.getWidth(dc);
double yscale;
if (scale != null)
yscale = scale * this.activeTexture.getHeight(dc);
else
yscale = this.activeTexture.getHeight(dc);
double maxwh = Math.max(xscale, yscale);
// The image is drawn using a parallel projection.
// This came from the fix in https://stackoverflow.com/questions/49637844/worldwind-pointplacemark-pitch
osh.pushProjectionIdentity(gl);
gl.glOrtho(0d, dc.getView().getViewport().width, 0d, dc.getView().getViewport().height, -0.6 * maxwh, 0.6 * maxwh);
// Apply the depth buffer but don't change it (for screen-space shapes).
if ((!dc.isDeepPickingEnabled()))
gl.glEnable(GL.GL_DEPTH_TEST);
gl.glDepthMask(false);
// Suppress any fully transparent image pixels.
gl.glEnable(GL2.GL_ALPHA_TEST);
gl.glAlphaFunc(GL2.GL_GREATER, 0.001f);
// Adjust depth of image to bring it slightly forward
double depth = opm.screenPoint.z - (8d * 0.00048875809d);
depth = depth < 0d ? 0d : (depth > 1d ? 1d : depth);
gl.glDepthFunc(GL.GL_LESS);
gl.glDepthRange(depth, depth);
// The image is drawn using a translated and scaled unit quad.
// Translate to screen point and adjust to align hot spot.
osh.pushModelviewIdentity(gl);
gl.glTranslated(opm.screenPoint.x + this.dx, opm.screenPoint.y + this.dy, 0);
Double heading = getActiveAttributes().getHeading();
Double pitch = this.computePitch(dc.getView());
// Adjust heading to be relative to globe or screen
if (heading != null)
{
if (AVKey.RELATIVE_TO_GLOBE.equals(this.getActiveAttributes().getHeadingReference()))
heading = dc.getView().getHeading().degrees - heading;
else
heading = -heading;
}
// Apply the heading and pitch if specified.
if (heading != null || pitch != null)
{
gl.glTranslated(xscale / 2, yscale / 2, 0);
if (pitch != null)
gl.glRotated(pitch, 1, 0, 0);
if (heading != null)
gl.glRotated(heading, 0, 0, 1);
gl.glTranslated(-xscale / 2, -yscale / 2, 0);
}
// Scale the unit quad
gl.glScaled(xscale, yscale, 1);
if (this.activeTexture.bind(dc))
dc.drawUnitQuad(activeTexture.getTexCoords());
gl.glDepthRange(0, 1); // reset depth range to the OGL default
if (this.mustDrawLabel())
{
if (!dc.isPickingMode() || this.isEnableLabelPicking())
this.drawLabel(dc, pickCandidates, opm);
}
}
finally
{
if (dc.isPickingMode())
{
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_TEXTURE_ENV_MODE, OGLUtil.DEFAULT_TEX_ENV_MODE);
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_SRC0_RGB, OGLUtil.DEFAULT_SRC0_RGB);
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_COMBINE_RGB, OGLUtil.DEFAULT_COMBINE_RGB);
}
gl.glDisable(GL.GL_TEXTURE_2D);
osh.pop(gl);
}
}
It will look like this:
What you want to achieve is scale your polygon based on the eye position of the camera, and keep the polygon oriented on the map.
You could try to update your second solution and add a RenderingListener ro update the size of your polygon before rendering:
wwd.addRenderingListener(new RenderingListener()
{
public void stageChanged(RenderingEvent event)
{
if (RenderingEvent.BEFORE_RENDERING.equals(event.getStage())
{
if (wwd.getView() != null && wwd.getView().getEyePosition() != null) {
// compute distance between eyePosition and object position, and set the scale.
}
}
}
});
I'm working with ARCore in Android Studio using java and am trying to implement ray intersection with an object.
I started with Google's provided sample (as found here: https://developers.google.com/ar/develop/java/getting-started).
Upon touching the screen, a ray gets projected and when this ray touches a Plane, a PlaneAttachment (with an Anchor/a Pose) is created in the intersection point.
I would then like to put a 3D triangle in the world attached to this Pose.
At the moment I create my Triangle based on the Pose's translation, like this:
In HelloArActivity, during onDrawFrame(...)
//Code from sample, determining the hits on planes
MotionEvent tap = mQueuedSingleTaps.poll();
if (tap != null && frame.getTrackingState() == TrackingState.TRACKING) {
for (HitResult hit : frame.hitTest(tap)) {
// Check if any plane was hit, and if it was hit inside the plane polygon.
if (hit instanceof PlaneHitResult && ((PlaneHitResult) hit).isHitInPolygon()) {
mTouches.add(new PlaneAttachment(
((PlaneHitResult) hit).getPlane(),
mSession.addAnchor(hit.getHitPose())));
//creating a triangle in the world
Pose hitPose = hit.getHitPose();
float[] poseCoords = new float[3];
hitPose.getTranslation(poseCoords, 0);
mTriangle = new Triangle(poseCoords);
}
}
}
Note: I am aware that the triangle's coordinates should be updated every time the Pose's coordinates get updated. I left this out as it is not part of my issue.
Triangle class
public class Triangle {
public float[] v0;
public float[] v1;
public float[] v2;
//create triangle around a given coordinate
public Triangle(float[] poseCoords){
float x = poseCoords[0], y = poseCoords[1], z = poseCoords[2];
this.v0 = new float[]{x+0.0001f, y-0.0001f, z};
this.v1 = new float[]{x, y+ 0.0001f, z-0.0001f};
this.v2 = new float[]{x-0.0001f, y, z+ 0.0001f};
}
After this, upon tapping the screen again I create a ray projected from the tapped (x,y) part of the screen, using Ian M his code sample provided in the answer to this question: how to check ray intersection with object in ARCore
Ray Creation, in HelloArActivity
/**
* Returns a world coordinate frame ray for a screen point. The ray is
* defined using a 6-element float array containing the head location
* followed by a normalized direction vector.
*/
float[] screenPointToWorldRay(float xPx, float yPx, Frame frame) {
float[] points = new float[12]; // {clip query, camera query, camera origin}
// Set up the clip-space coordinates of our query point
// +x is right:
points[0] = 2.0f * xPx / mSurfaceView.getMeasuredWidth() - 1.0f;
// +y is up (android UI Y is down):
points[1] = 1.0f - 2.0f * yPx / mSurfaceView.getMeasuredHeight();
points[2] = 1.0f; // +z is forwards (remember clip, not camera)
points[3] = 1.0f; // w (homogenous coordinates)
float[] matrices = new float[32]; // {proj, inverse proj}
// If you'll be calling this several times per frame factor out
// the next two lines to run when Frame.isDisplayRotationChanged().
mSession.getProjectionMatrix(matrices, 0, 1.0f, 100.0f);
Matrix.invertM(matrices, 16, matrices, 0);
// Transform clip-space point to camera-space.
Matrix.multiplyMV(points, 4, matrices, 16, points, 0);
// points[4,5,6] is now a camera-space vector. Transform to world space to get a point
// along the ray.
float[] out = new float[6];
frame.getPose().transformPoint(points, 4, out, 3);
// use points[8,9,10] as a zero vector to get the ray head position in world space.
frame.getPose().transformPoint(points, 8, out, 0);
// normalize the direction vector:
float dx = out[3] - out[0];
float dy = out[4] - out[1];
float dz = out[5] - out[2];
float scale = 1.0f / (float) Math.sqrt(dx*dx + dy*dy + dz*dz);
out[3] = dx * scale;
out[4] = dy * scale;
out[5] = dz * scale;
return out;
}
The result of this however is that, no matter where I tap the screen, it always counts as a hit (regardless of how much distance I add between the points, in Triangle's constructor).
I suspect this has to do with how a Pose is located in the world, and using the Pose's translation coordinates as a reference point for my triangle is not the way to go, so I'm looking for the correct way to do this, but any remarks regarding other parts of my method are welcome!
Also I have tested my method for ray-triangle intersection and I don't think it is the problem, but I'll include it here for completeness:
public Point3f intersectRayTriangle(CustomRay R, Triangle T) {
Point3f I = new Point3f();
Vector3f u, v, n;
Vector3f dir, w0, w;
float r, a, b;
u = new Vector3f(T.V1);
u.sub(new Point3f(T.V0));
v = new Vector3f(T.V2);
v.sub(new Point3f(T.V0));
n = new Vector3f(); // cross product
n.cross(u, v);
if (n.length() == 0) {
return null;
}
dir = new Vector3f(R.direction);
w0 = new Vector3f(R.origin);
w0.sub(new Point3f(T.V0));
a = -(new Vector3f(n).dot(w0));
b = new Vector3f(n).dot(dir);
if ((float)Math.abs(b) < SMALL_NUM) {
return null;
}
r = a / b;
if (r < 0.0) {
return null;
}
I = new Point3f(R.origin);
I.x += r * dir.x;
I.y += r * dir.y;
I.z += r * dir.z;
return I;
}
Thanks in advance!
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.
I’m relatively new to ImageMagick and java and working on a project to display text around the outside of a circle centered at 0 degrees on the circle using ImageMagick 6.3.9 Q16 and jmagick 6.3.9 Q16 on windows. We’re porting existing image magick code that does this from PHP MagickWand but the placement of each letter on the arc of the circle comes out a little off in the java version I think because of the following difference.
In MagickWand, it’s placed on the arc by this one line of code which uses a float x, y coordinate value and float angle value (for greater precision) for annotating the drawing wand (equivalent of DrawInfo in jmagick) and works beautifully:
MagickAnnotateImage($magick_wand, $drawing_wand, $origin_x + $x, $origin_y - $y, $angle, $character);
In jmagick though, the annotateImage method only takes one argument which is the DrawInfo so I ended up with what I think is the only other alternative, the compositeImage method. So in order to do that, I’m drawing each character as separate draw info, then annotating that to a transparent png image, then rotating that image via rotateImage method, then using compositeImage to place it on my canvas image but compositeImage only deals with x & y as int values (and doesn’t consider angle) so I’m rounding my x & y double values (to get same number of decimals or more like php version is using just to rule that out) at that point which I suspect is the main reason it’s placing the characters a little off on the circle.
My code performing the work is the following where Article is a local path to a font file (ex: E:\WCDE_ENT70\workspace\Stores\WebContent\AdminArea\CoordsCenterSection\fonts\ARIALN.TTF), nameNumStr is the string to render on the circle (ex: SAMUELSON), fsize is the point size of the font (ex: 32), colorStr is font color name (ex: black), radVal is radius (ex: 120), poix is x origin start coordinate (ex: 150), poiy is y origin start coordinate (ex: 150):
public byte[] getArcedImage(String Article, String nameNumStr, int fsize, String colorStr, int radVal, int poix, int poiy)
{
try {
Font f = null;
try {
f = Font.createFont(Font.TRUETYPE_FONT, new FileInputStream(Article.replaceAll("%20"," ")));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (FontFormatException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
String fontName = f.getName();
// Use awt's font metrics since jmagick doesn't have font metrics built in like php magickwand does
FontMetrics fm = createFontMetrics(new Font(fontName, Font.PLAIN, fsize));
int strImgW = fm.stringWidth(nameNumStr);
int strImgH = fm.getHeight();
String spacerImg = "E:\\WCDE_ENT70\\workspace\\Stores\\WebContent\\AdminArea\\CoordsCenterSection\\images\\600x600.png";
//Read in large 600 png first as our main canvas
ImageInfo bi = new ImageInfo(spacerImg);
MagickImage bmi = new MagickImage(bi);
// Make canvas image transparent
bmi.setMatte(true);
bmi.setBackgroundColor(PixelPacket.queryColorDatabase("#FFFF8800"));
//defaults or param vals
final int radius = radVal;
final int origin_x = poix;
final int origin_y = poiy;
final int center_text_on = 0;
final int charXGeom = 150;
final int charYGeom = 150;
double circumference = 0;
double percentage = 0;
double degrees = 0;
double start = 0;
double current_degree = 0;
double angle = 0;
double angle_adjustment = 0;
double character_center = 0;
/**
* Calculate the circumference of the drawn circle and label the image
* with it.
*/
circumference = (2 * Math.PI * radius);
/**
* Calculate the percentage of the circumference that the string will
* consume.
*/
percentage = strImgW / circumference;
/**
* Convert this percentage into something practical - degrees.
*/
degrees = 360 * percentage;
/**
* Because the string is centered, we need to calculate the starting point
* of the string by subtracting half of the required degrees from the
* anticipated center mark.
*/
start = center_text_on - (degrees / 2);
/**
* Initialize our traversal starting point.
*/
current_degree = start;
//
ImageInfo ci = null;
MagickImage cmi = null;
double x = 0;
double y = 0;
int finalStrWidth = 0;
int charImgW = 0;
int charImgH = 0;
for (int i=0; i<nameNumStr.length(); i++)
{
/**
* Isolate the appropriate character.
*/
String charVal = nameNumStr.substring(i, i+1);
charImgW = fm.stringWidth(charVal);
charImgH = strImgH;
ci = new ImageInfo(spacerImg);
cmi = new MagickImage(ci);
// Create Rectangle for cropping character image canvas to final width and height
Rectangle charRect = new Rectangle(0,0,charImgW,charImgH);
// Crop image to final width and height
cmi = cmi.cropImage(charRect);
// Make image transparent
cmi.setMatte(true);
cmi.setBackgroundColor(PixelPacket.queryColorDatabase("#FFFF8800"));
// Set a draw info for each character
DrawInfo cdi = new DrawInfo(ci);
// Set Opacity
cdi.setOpacity(0);
// Set Gravity
cdi.setGravity(GravityType.CenterGravity);
// Set Fill Color
cdi.setFill(PixelPacket.queryColorDatabase(colorStr));
// Set Font Size
cdi.setPointsize(fsize);
// Set Font
cdi.setFont(Article.replaceAll("%20"," "));
// Set the text
cdi.setText(charVal);
// Make the text smoother
cdi.setTextAntialias(true);
// Annotate the draw info to make the character image
cmi.annotateImage(cdi);
// For debug purposes
finalStrWidth += charImgW;
/**
* Calculate the percentage of the circumference that the character
* will consume.
*/
percentage = charImgW / circumference;
/**
* Convert this percentage into something practical - degrees.
*/
degrees = 360 * percentage;
/**
* Calculate the x and y axis adjustments to make, based on the origin
* of the circle, so we can place each letter.
*/
x = radius * Math.sin(Math.toRadians(current_degree));
y = radius * Math.cos(Math.toRadians(current_degree));
// Rotate the character image to the angle
cmi = cmi.rotateImage(angle);
// Composite character image to main canvas image
bmi.compositeImage(CompositeOperator.HardLightCompositeOp, cmi, (int)Math.round((origin_x+x)), (int)Math.round((origin_y-y)));
// Increment the degrees
current_degree += degrees;
}
bmi = bmi.trimImage();
byte[] pi = bmi.imageToBlob(ci);
return pi;
} catch (MagickException e) {
e.printStackTrace();
return null;
}
}
private FontMetrics createFontMetrics(Font font)
{
BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB_PRE);
Graphics g = bi.getGraphics();
FontMetrics fm = g.getFontMetrics(font);
g.dispose();
bi = null;
return fm;
}
private Rectangle2D createFontRectangle(Font font, String strVal)
{
BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB_PRE);
Graphics g = bi.getGraphics();
FontMetrics fm = g.getFontMetrics(font);
Rectangle2D rect = fm.getStringBounds(strVal, g);
g.dispose();
bi = null;
return rect;
}
I've since found that it’s possible to use DrawInfo’s setGeometry method to set the x, y and saw on the one example I found on jmagick.org's wiki that it supposedly can be used for much more than x, y placement but can’t find any other examples or documentation showing how else it can be used (hopefully for specifying an angle as well).
I’m not positive but it seems that setGeometry would be the only way to potentially specify an angle since jmagick’s implementation of annotateImage only takes a Draw Info as it’s argument.
Does anyone know a way to use DrawInfo’s setGeometry method to set the x, y and angle? I think it may solve my problem. Also, if anyone has any working example of using jmagick to draw text around a circle that they’d be willing to share, I’d be greatly appreciative.
Thanks