WorldWindEarth / WorldWindJava

A community supported fork of the NASA WorldWind Java SDK (WWJ) is for building cross-platform 3D geospatial desktop applications in Java.
https://worldwind.earth/WorldWindJava/
48 stars 14 forks source link

Added SurfaceText rotation #40

Closed ComBatVision closed 5 years ago

ComBatVision commented 5 years ago

Description of the Change

Added the capability to rotate SurfaceText objects.

Why 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. Знімок екрану з 2019-04-19 21-41-09

Note: See the conversation on this pull requests for more information on the fix.

Applicable Issues

Closes #37

ComBatVision commented 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.

Знімок екрану з 2019-04-19 22-00-11

PJHogan commented 5 years ago

Eugene (Sufaev), this one is for you! https://www.youtube.com/watch?v=0Ph89xbv0Kw

wcmatthysen commented 5 years ago

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:

without-fix

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:

with-fix

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:

with-fix-no-rotation

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.

ComBatVision commented 5 years ago

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: Знімок екрану з 2019-04-20 12-25-31

ComBatVision commented 5 years ago

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.
Знімок екрану з 2019-04-20 13-20-53

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;
wcmatthysen commented 5 years ago

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.

wcmatthysen commented 5 years ago

@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.

wcmatthysen commented 5 years ago

@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:

rotated-left-aligned

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:

not-rotated-left-aligned

I'm currently taking a look at that.

wcmatthysen commented 5 years ago

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:

snug-fit

But, if you rotate the text, the fit is too snug again and you get this:

too-snug-fit

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.

wcmatthysen commented 5 years ago

@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:

fixed-rotations

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.

ComBatVision commented 5 years ago

Thanks. Looks perfect! Can we merge this solution to develop?

wcmatthysen commented 5 years ago

@Sufaev, I'm just moving the changes in SurfaceTextUsage out to a new class in the functional tests folder.

wcmatthysen commented 5 years ago

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.

emxsys commented 5 years ago

@wcmatthysen @Sufaev I'll perform a sanity check and merge the changes today.

PJHogan commented 5 years ago

https://youtu.be/qpGx4foRdPw