Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EllipsoidRhumb class similar to EllipsoidGeodesic #7484

Merged
merged 12 commits into from
Jan 18, 2019

Conversation

shehzan10
Copy link
Member

As the title suggest, this PR adds an EllipsoidRhumb class similar to EllipsoidGeodesic. This is the first step in adding rhumb line support for geometry (#4000).

@cesium-concierge
Copy link

Thanks for the pull request @shehzan10!

  • ✔️ Signed CLA found.
  • CHANGES.md was not updated.
    • If this change updates the public API in any way, please add a bullet point to CHANGES.md.

Reviewers, don't forget to make sure that:

  • Cesium Viewer works.
  • Works in 2D/CV.
  • Works (or fails gracefully) in IE11.

I am a bot who helps you make Cesium awesome! Contributions to my configuration are welcome.

🌍 🌎 🌏

@shehzan10
Copy link
Member Author

@likangning93 Can you do the initial review?

@pjcozzi
Copy link
Contributor

pjcozzi commented Jan 15, 2019

Are you sure about the name? Why isn't this EllipsoidRhumbLine?

@likangning93
Copy link
Contributor

Are you sure about the name? Why isn't this EllipsoidRhumbLine?

I think most dictionaries consider the term rhumb to imply the arc/line. It's the same as saying geodesic instead of geodesic line.

On the other hand the Wikipedia entry argues for loxodrome, but it also admits that modern usage is synonymous, so I think shorter is better in this case ;)

@pjcozzi
Copy link
Contributor

pjcozzi commented Jan 15, 2019

Please double check how the term is used in our field; in the past 15 years, I have only heard people say "rhumb line", not "rhumb" - it's OK if it really is commonly called "rhumb" - just be sure please.

@shehzan10
Copy link
Member Author

I think most dictionaries consider the term rhumb to imply the arc/line. It's the same as saying geodesic instead of geodesic line.

@pjcozzi This was my exact reasoning behind the name, that's geodesic is EllipsoidGeodesic and not EllipsoidGeodesicLine, so I made it EllipsoidRhumb accordingly.

I'm seeing uses of both in API references (I'm guessing others chose the shorter name for the same reason). One notable use is STK Components which uses EllipsoidRhumbLine http://help.agi.com/AGIComponents/html/T_AGI_Foundation_Geometry_Shapes_EllipsoidRhumbLine.htm

Should I change it to EllipsoidRhumbLine?

@pjcozzi
Copy link
Contributor

pjcozzi commented Jan 15, 2019

Should I change it to EllipsoidRhumbLine?

Seems that way even though it does not have perfect symmetry with EllipsoidGeodesic; these are the household names AFAIK.

Copy link
Contributor

@likangning93 likangning93 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't say I was able to parse much of the math, but some comments in other places:

Source/Core/EllipsoidRhumbLine.js Outdated Show resolved Hide resolved
Source/Core/EllipsoidRhumbLine.js Outdated Show resolved Hide resolved
Source/Core/EllipsoidRhumbLine.js Outdated Show resolved Hide resolved
Source/Core/EllipsoidRhumbLine.js Outdated Show resolved Hide resolved
Source/Core/EllipsoidRhumbLine.js Outdated Show resolved Hide resolved
Specs/Core/EllipsoidRhumbLineSpec.js Outdated Show resolved Hide resolved
Specs/Core/EllipsoidRhumbLineSpec.js Show resolved Hide resolved
Specs/Core/EllipsoidRhumbLineSpec.js Outdated Show resolved Hide resolved
Specs/Core/EllipsoidRhumbLineSpec.js Show resolved Hide resolved
Specs/Core/EllipsoidRhumbLineSpec.js Outdated Show resolved Hide resolved
@shehzan10
Copy link
Member Author

@likangning93 Updated.

@shehzan10
Copy link
Member Author

@likangning93 I added 2 new functions to compute intersections.

@likangning93
Copy link
Contributor

likangning93 commented Jan 15, 2019

I threw together a crude toy to play with rhumbs vs. geodesics in Sandcastle, and it looks like the lines work as advertised:

var viewer = new Cesium.Viewer('cesiumContainer', {
    mapProjection : new Cesium.WebMercatorProjection()
});

var entities = viewer.entities;

var drawing = false;
var startPoint;

var handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas);
handler.setInputAction(function(event) {
    if (drawing) {
        drawing = false;
        return;
    }

    var earthPosition = viewer.camera.pickEllipsoid(event.position);

    if (Cesium.defined(earthPosition)) {
        drawing = true;
        startPoint = Cesium.Cartographic.fromCartesian(earthPosition);
    }
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);

handler.setInputAction(function(event) {
    if (!drawing) {
        return;
    }

    var newPosition = viewer.camera.pickEllipsoid(event.endPosition);
    if (Cesium.defined(newPosition)) {
        var endPoint = Cesium.Cartographic.fromCartesian(newPosition);
        if (!Cesium.Cartographic.equalsEpsilon(endPoint, startPoint, Cesium.Math.EPSILON3)) {
            makeLines(startPoint, endPoint);
        }
    }
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

function makeLines(start, end) {
    entities.removeAll();

    // create regular line
    entities.add({
        name : 'Geodesic',
        polyline : {
            positions : Cesium.Cartesian3.fromRadiansArray([start.longitude, start.latitude,
                                                            end.longitude, end.latitude]),
            width : 5,
            material : Cesium.Color.RED
        }
    });

    var rhumb = new Cesium.EllipsoidRhumbLine(start, end);

    var radians = [];
    var positionsCount = 100;
    for (var i = 0; i <= positionsCount; i++) {
        var interpolated = rhumb.interpolateUsingFraction(i / positionsCount);
        radians.push(interpolated.longitude);
        radians.push(interpolated.latitude);
    }

    entities.add({
        name : 'Rhumbline',
        polyline : {
            positions : Cesium.Cartesian3.fromRadiansArray(radians),
            width : 5,
            material : Cesium.Color.BLUE
        }
    });
}

rhumbfun

Source/Core/EllipsoidRhumbLine.js Outdated Show resolved Hide resolved
Source/Core/EllipsoidRhumbLine.js Outdated Show resolved Hide resolved
Source/Core/EllipsoidRhumbLine.js Outdated Show resolved Hide resolved
result.height = 0;
return result;
} else if (CesiumMath.equalsEpsilon(Math.abs(CesiumMath.PI_OVER_TWO - absHeading), CesiumMath.PI_OVER_TWO, CesiumMath.EPSILON8)) {
if (CesiumMath.equalsEpsilon(intersectionLongitude, start.longitude, CesiumMath.EPSILON12)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be !CesiumMath.equalsEpsilon(...?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this is correct. It's else if is checking if the line is N-S, and the nested if is checking if the longitude is the same, in which case there are infinite intersections.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm ok. So this case is "infinite intersections" and returns undefined, and the path below returns the intersection at the pole when considering the rhumbline to be an infinite spiral defined by a start and a heading? If so, that might be worth noting in the doc.

var eSinPhi = ellipticity * Math.sin(phi);
var numerator = (1 + eSinPhi) / (1 - eSinPhi);
newPhi = 2 * Math.atan(leftComponent * Math.pow(numerator / denominator, ellipticity / 2)) - CesiumMath.PI_OVER_TWO;
} while (!CesiumMath.equalsEpsilon(newPhi, phi, CesiumMath.EPSILON12));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if there's a situation where this wouldn't converge, and we would want this to break after some number of iterations?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will converge and it's converging pretty quickly in all the tests I did. In the test cases I ran, it took less than 20 iterations.

Specs/Core/EllipsoidRhumbLineSpec.js Outdated Show resolved Hide resolved
Specs/Core/EllipsoidRhumbLineSpec.js Outdated Show resolved Hide resolved
expect(pointUsingInterpolation.longitude).toEqualEpsilon(pointUsingIntersection.longitude, CesiumMath.EPSILON12);
expect(pointUsingInterpolation.latitude).toEqualEpsilon(pointUsingIntersection.latitude, CesiumMath.EPSILON12);

pointUsingInterpolation = rhumb.interpolateUsingFraction(1.1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the expected behavior here? I guess does it clamp to the endpoint?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will interpolate beyond the endpoint.

Specs/Core/EllipsoidRhumbLineSpec.js Show resolved Hide resolved
Specs/Core/EllipsoidRhumbLineSpec.js Outdated Show resolved Hide resolved
@shehzan10
Copy link
Member Author

@likangning93 Updated.

@likangning93
Copy link
Contributor

Maybe document the "infinite spiral from start+heading" behavior, since that might change expectations for N-S rhumb/longitude intersection and interpolation beyond length. Otherwise that should be it for me, this looks good @shehzan10!

@shehzan10
Copy link
Member Author

Thanks @likangning93. I added the comment.

@mramato Do you want to take a look?

@mramato
Copy link
Contributor

mramato commented Jan 16, 2019

Will do later today or first thing tomorrow.

heading : {
get : function() {
//>>includeStart('debug', pragmas.debug);
Check.defined('distance', this._distance);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is what EllipsoidGeodesic has, but this is a really odd check to me, especially since there is no public property called distance. Would it be better for us to update this and EllipsoidGeodesic to just be undefined? On the other hand, it may never happen in practice so I could be bikeshedding. I'm okay with just punting, but since EllipsoidGeodesic is so old, it's good to revisit things once in a while.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was just a way to assure that heading and distance are both computed since the constructor allows using undefined as start and end.

We could simply change this to check whether heading is defined, which will make more sense here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since EllipsoidGeodescic has been like this forever, I'm probably bike shedding, let's just leave it as-is and we can worry about it if it ever comes up.

* @param {EllipsoidRhumbLine} [result] The object in which to store the result.
* @returns {EllipsoidRhumbLine} The EllipsoidRhumbLine object.
*/
EllipsoidRhumbLine.fromStartAndEnd = function(start, end, ellipsoid, result) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this function needed, it just matches the constructor, doesn't it??

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it because it allows using a scratch result object.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like overkill, can't you just call setEndPoints on an existing object? I doubt someone would need to change the ellipsoid on an existing instance. If we have a valid use case, then maybe it's worth it, but we try not to add code "just because it might be useful" since that introduces more code to maintain.

result._heading = CesiumMath.negativePiToPi(heading);
result._distance = distance;

result._end = result.interpolateUsingSurfaceDistance(distance, new Cartographic());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole function is kind of an odd implementation to me. Why not break out interpolateUsingSurfaceDistance into a standalone local private function that can be called with start, heading, distance, ellipsoid, us that to compute the end point then just create the fully contructed result. Using a half-initialized instance like this seems like something that could bite us.

Obviously we still expose the prototype version of interpolateUsingSurfaceDistance that just called the local one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this could work better. I'll make the change.

result.latitude = start.latitude;
result.height = 0;
return result;
} else if (CesiumMath.equalsEpsilon(Math.abs(CesiumMath.PI_OVER_TWO - absHeading), CesiumMath.PI_OVER_TWO, CesiumMath.EPSILON8)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume these epsilons were from the ported code? (if not, how did we choose them?) Same for the rest of the PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it was chosen as the epsilon at which Math.cos() returns 1.0 similar to http://help.agi.com/AGIComponents/html/F_AGI_Foundation_Trig_AngleEpsilon.htm.

@mramato
Copy link
Contributor

mramato commented Jan 17, 2019

Plese merge in master when you get a chance.

@mramato
Copy link
Contributor

mramato commented Jan 17, 2019

Thanks @shehzan10, tests look comprehensive (but I can't run coverage right now since I'm on Linux).

Those few comments are all I have, bump when ready and I'll merge.

Looking forward to the follow-up PR for geometries!


/**
* Initializes a rhumb line on the ellipsoid connecting the two provided planetodetic points.
*

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should document behavior when start and end are on opposite meridians (returns E-W line). Should there be a mechanism to get the opposite line?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GatorScott This check does not allow points to be on opposite ends meridians (same limitation as EllipsoidGeodesic).

https://github.com/AnalyticalGraphicsInc/cesium/blob/c323623a52b7b7b318421ffbb09738a4d9727d5e/Source/Core/EllipsoidRhumbLine.js#L159

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this limitation/restriction be documented so the developer can protect themselves from this DeveloperError: "Expected value to be greater than or equal to 0.0125, actual value was 0.0"? Secondarily, is 0.716197 degrees the right epsilon?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the documentation for the developer error. I'm not entirely certain about how 0.0125 came to be, which is from the STK Components code. @fstoner anything you can add?

@shehzan10
Copy link
Member Author

@mramato Updated.

@mramato
Copy link
Contributor

mramato commented Jan 18, 2019

Looks good, just document the exception that @GatorScott mentioned and this should be good to go.

@mramato
Copy link
Contributor

mramato commented Jan 18, 2019

Thanks again, @shehzan10.

@mramato mramato merged commit 38efff5 into CesiumGS:master Jan 18, 2019
@shehzan10 shehzan10 deleted the ellipsoid-rhumb branch January 18, 2019 15:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants