Closed ComBatVision closed 5 years ago
There is no truncation issue in some bigger zoom.
Probably it is not related with rotation itself, but with displaying text on different scaled tiles.
Eugene (Sufaev), this one is for you! https://www.youtube.com/watch?v=0Ph89xbv0Kw
I think I found the issue with the text being cut off like that. The bounding-sector calculation should be updated to take the rotation into account. If you rotate the text then the bounding-sector should enlarge if rotated at 45-degrees. If the bounding-sector is too small then the cut-off will occur. To see the bounding-sector for the SurfaceText
object you can enable the debug drawing logic as:
surfaceText.setDrawBoundingSectors(true);
This will cause a white rectangle to be drawn around it that looks like:
You can see from image that the rectangle is not rotated. This causes the clipping issue.
To rotate the rectangle, you need to modify the computeSector()
method in SurfaceText
to look like:
protected Sector[] computeSector(DrawContext dc)
{
// Compute text extent depending on distance from eye
Globe globe = dc.getGlobe();
double widthInPixels = this.textBounds.getWidth();
double heightInPixels = this.textBounds.getHeight();
double heightInMeters = this.textSizeInMeters;
double widthInMeters = heightInMeters * (widthInPixels / heightInPixels);
double radius = globe.getRadius();
double heightInRadians = heightInMeters / radius;
double widthInRadians = widthInMeters / radius;
// Compute the offset from the reference position. Convert pixels to meters based on the geographic size
// of the text.
Point2D point = this.getOffset().computeOffset(widthInPixels, heightInPixels, null, null);
double metersPerPixel = heightInMeters / heightInPixels;
double dxRadians = (point.getX() * metersPerPixel) / radius;
double dyRadians = (point.getY() * metersPerPixel) / radius;
double centerLat = this.location.latitude.addRadians(dyRadians + 0.5 * heightInRadians).degrees;
double centerLon = this.location.longitude.addRadians(dxRadians + 0.5 * widthInRadians).degrees;
LatLon center = LatLon.fromDegrees(centerLat, centerLon);
double hw = widthInMeters / 2.0;
double hh = heightInMeters / 2.0;
double distance = Math.sqrt(hw * hw + hh * hh);
double pathLength = distance / radius;
double halfPI = Math.PI / 2.0;
double azimuth1 = halfPI - (Math.atan2(-hh, -hw) - this.heading.radians);
LatLon corner1 = LatLon.greatCircleEndPosition(center, azimuth1, pathLength);
double azimuth2 = halfPI - (Math.atan2(-hh, hw) - this.heading.radians);
LatLon corner2 = LatLon.greatCircleEndPosition(center, azimuth2, pathLength);
double azimuth3 = halfPI - (Math.atan2(hh, hw) - this.heading.radians);
LatLon corner3 = LatLon.greatCircleEndPosition(center, azimuth3, pathLength);
double azimuth4 = halfPI - (Math.atan2(hh, -hw) - this.heading.radians);
LatLon corner4 = LatLon.greatCircleEndPosition(center, azimuth4, pathLength);
Sector boundingSector = Sector.boundingSector(Arrays.asList(corner1, corner2, corner3, corner4));
double minLat = boundingSector.getMinLatitude().degrees;
double maxLat = boundingSector.getMaxLatitude().degrees;
double minLon = boundingSector.getMinLongitude().degrees;
double maxLon = boundingSector.getMaxLongitude().degrees;
this.drawLocation = LatLon.fromDegrees(minLat, minLon);
if (maxLon > 180) {
// Split the bounding box into two sectors, one to each side of the anti-meridian.
Sector[] sectors = new Sector[2];
sectors[0] = Sector.fromDegrees(minLat, maxLat, minLon, 180);
sectors[1] = Sector.fromDegrees(minLat, maxLat, -180, maxLon - 360);
this.spansAntimeridian = true;
return sectors;
} else {
this.spansAntimeridian = false;
return new Sector[] {Sector.fromDegrees(minLat, maxLat, minLon, maxLon)};
}
}
The fix was done by borrowing some logic from SurfaceQuad
. In particular, if you look at the getLocations()
method you'll see how to calculated rotated positions. This results in the bounding-sector being calculated as shown here:
However, the bounding-sector is not 100% accurate. If you set the heading to 0-degrees so that the text is not rotated, you get a bounding-sector that looks like:
If you remove the rotation fix for the bounding-sector you'll see that the bounding-sector for a 0-degrees heading rotation is perfectly calculated (it fits perfectly around the text). I'm not too sure what the implications of this is. If @emxsys can confirm, I think this is good enough and you can use the fix.
I have changed attribute name and type to protected Angle heading = Angle.ZERO;
.
Do not merge this PR until bounding sector issue will be solved.
It has not only wrong width calculation, but also do not process offset correctly.
I am working to solve both issues.
Bellow is example with Offset.LEFT_CENTER:
Could anybody help me with offset calculation in bounding sector? I am stuck. I have tested all types of offset and I do not understand why text is located in unexpected positions (especially Offset.RIGHT_CENTER) or I do not correctly understand the offset meaning.
I also do not understand why Math.atan2 returns absolutely correct bounding box and text corners with 90 degree heading, but for some reasons it returns wider bounding box when heading become closer to 0 degrees, but with correct height.
I have little bit optimized proposed sector calculation:
double hw = 0.5 * widthInRadians;
double hh = 0.5 * heightInRadians;
double centerLon = this.location.longitude.addRadians(dxRadians + hw).degrees;
double centerLat = this.location.latitude.addRadians(dyRadians + hh).degrees;
LatLon center = LatLon.fromDegrees(centerLat, centerLon);
double pathLength = Math.sqrt(hw * hw + hh * hh);
double azimuth1 = Math.atan2(-hw, -hh) - this.heading.radians;
LatLon corner1 = LatLon.greatCircleEndPosition(center, azimuth1, pathLength);
double azimuth2 = Math.atan2(-hw, hh) - this.heading.radians;
LatLon corner2 = LatLon.greatCircleEndPosition(center, azimuth2, pathLength);
double azimuth3 = Math.atan2(hw, hh) - this.heading.radians;
LatLon corner3 = LatLon.greatCircleEndPosition(center, azimuth3, pathLength);
double azimuth4 = Math.atan2(hw, -hh) - this.heading.radians;
LatLon corner4 = LatLon.greatCircleEndPosition(center, azimuth4, pathLength);
Sector boundingSector = Sector.boundingSector(Arrays.asList(corner1, corner2, corner3, corner4));
double minLat = boundingSector.getMinLatitude().degrees;
double maxLat = boundingSector.getMaxLatitude().degrees;
double minLon = boundingSector.getMinLongitude().degrees;
double maxLon = boundingSector.getMaxLongitude().degrees;
Hi @Sufaev, I'll have a look for you as well. I think the offset needs to be taken into account when calculating the bounding-sector.
@Sufaev, I looked a bit at the offset calculation. The offset-constants specified in the Offset
class like RIGHT_CENTER
won't work with SurfaceText
as these offset values are taken from the bottom left corner. For example the center location is:
public static final Offset CENTER = Offset.fromFraction(0.5, 0.5);
whereas if you look at the default offset of SurfaceText
you have:
public static final Offset DEFAULT_OFFSET = new Offset(-0.5d, -0.5d, AVKey.FRACTION, AVKey.FRACTION);
This means that the offset for SurfaceText
is taken relative to the center-point and calculates the bottom-left corner where the text starts.
That is why you were getting strange results when you used Offset.RIGHT_CENTER
.
@Sufaev, I found an issue with the rotation logic. You need to modify applyDrawTransform()
method as follows:
/**
* Apply a transform to the GL state to draw the text at the proper location and scale.
*
* @param dc Current draw context.
* @param sdc Current surface tile draw context.
*/
protected void applyDrawTransform(DrawContext dc, SurfaceTileDrawContext sdc)
{
Vec4 point = new Vec4(this.location.getLongitude().degrees, this.location.getLatitude().degrees, 1);
// If the text box spans the anti-meridian and we're drawing tiles to the right of the anti-meridian, then we
// need to map the translation into coordinates relative to that side of the anti-meridian.
if (this.spansAntimeridian &&
Math.signum(sdc.getSector().getMinLongitude().degrees) != Math.signum(this.drawLocation.longitude.degrees)) {
point = new Vec4(this.location.getLongitude().degrees - 360, this.location.getLatitude().degrees, 1);
}
point = point.transformBy4(sdc.getModelviewMatrix());
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
// Translate to location point
gl.glTranslated(point.x(), point.y(), point.z());
// Apply the scaling factor to draw the text at the correct geographic size
gl.glScaled(this.scale, this.scale, 1d);
double widthInPixels = this.textBounds.getWidth();
double heightInPixels = this.textBounds.getHeight();
gl.glTranslated(widthInPixels / 2, heightInPixels / 2, 0);
// Apply rotation angle
gl.glRotated(-this.heading.degrees, 0, 0, 1);
gl.glTranslated(-widthInPixels / 2, -heightInPixels / 2, 0);
}
This ensures that the text is rotated about its center-point before being offset.
Then, if I rotate 45-degrees and I offset so that the text is left-aligned and centered vertically:
surfaceText.setHeading(Angle.fromDegrees(45));
surfaceText.setOffset(Offset.fromFraction(0.0, -0.5));
you will get the following bounding-sector:
This is very close to what we want. I'm still having issues with the slightly larger bounding-sector. Thus, if I rotate 0-degrees (and left-align as before) I still get the following:
I'm currently taking a look at that.
I think the bounding-sector overshoot happens because of the way the path-length is calculated in the computeSector()
method:
double distance = Math.sqrt(hw * hw + hh * hh);
double pathLength = distance / radius;
If we change this to:
double pathLength = LatLon.greatCircleDistance(this.location, center).radians;
You get a bounding-sector that fits more snugly around the text again:
But, if you rotate the text, the fit is too snug again and you get this:
Correct me if I'm wrong @emxsys, but I think the slightly larger bounding-sector won't cause any noticeable problems? On the other hand, I think a bounding-sector that is too small will cause text to be clipped like we saw initially.
@Sufaev, I managed to fix the SurfaceText
rotation. I now perform a calculation to get new pixel width and height values and work with those. The previous changes took the latitude and longitude values of the corners and rotated them which wasn't 100% accurate (hence the slightly larger bounding sector).
If you pull the latest changes I made in your repository you'll see this when you run SurfaceTextUsage
:
I iterate over 30-degree angle intervals and over all offsets. The location of each SurfaceText
object is highlighted with a red-dot PointPlacemark
. This demonstrates where it is actually placed, and how the text is offset relative to this position.
Thanks. Looks perfect! Can we merge this solution to develop?
@Sufaev, I'm just moving the changes in SurfaceTextUsage
out to a new class in the functional tests folder.
OK, I've moved the test class out to the testFunctional
folder. It is now called SurfaceTextTest
. The original SurfaceTextUsage
was reverted to its previous state. I think it is better to keep this in the testFunctional
folder along the other functional-tests. However, I see that the ant-scripts to not compile the functional-tests. We should probably change that as well, so that we can actually run the functional-tests.
@emxsys, I think we can merge this into the development branch. If you can just cross-check and confirm that everything is in order.
@wcmatthysen @Sufaev I'll perform a sanity check and merge the changes today.
Description of the Change
Added the capability to rotate
SurfaceText
objects.heading
property to theSurfaceText
classWhy Should This Be In Core?
Enhances the SDK's labeling features.
Benefits
This improvement adds
SurfaceText
rotation capability.Potential Drawbacks
Note: The following truncation issue was resolved through collaboration with @wcmatthysen on this pull request.
Initially, the only issue found - big texts are truncated by tile borders.
Note: See the conversation on this pull requests for more information on the fix.
Applicable Issues
Closes #37