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

Proj4JS and Custom Projections in Columbus View #6986

Merged
merged 17 commits into from
Nov 1, 2018

Conversation

likangning93
Copy link
Contributor

@likangning93 likangning93 commented Aug 31, 2018

This PR adds support for Proj4js projections and user-defined projections in 2D and Columbus View.

This still needs a lot of testing - opening a PR for early feedback on the general approach before going much further.

Known Problems/Open Questions

* [ ] projection/unprojection sometimes fails, currently caught and logged the first time, then silent no longer reproducible thanks to proj4js improvements?
* [ ] wraparound in 2D broken for many projections default to rotatable 2D for proj4/custom projections
* [ ] What to do about projections that split the globe somewhere other than the antimeridian? shouldn't be a problem in most cases
* [ ] workers currently constantly spinning up new Projection objects, where is a good place to "cache" the projection?

  • what do weird projections do to terrain LOD selection? (see EPSG 3411 below)

TODO

* [ ] assess when proj4js gets loaded, how well it gzips - [comment](#6986 (comment))

  • handling for height beyond just "use the same heights"
  • profile terrain projection performance

Pretty Pictures

terrain_reprojection_global
mollweide

terrain_reprojection_local
Mt. St. Helens on mollweide

polar
EPSG 3411 (polar)

custom
Nonsensical custom projection

@cesium-concierge
Copy link

cesium-concierge commented Aug 31, 2018

Thanks for the pull request @likangning93!

  • ✔️ 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.

🌍 🌎 🌏

@likangning93
Copy link
Contributor Author

Works (or fails gracefully) in IE11.

Getting an Access is denied from Resource.js when running the Custom Projection Sandcastle in IE11, not 100% sure what's going on here. Is Data URI use like this not allowed in IE11?

accessdenied

@mramato
Copy link
Contributor

mramato commented Aug 31, 2018

This sounds vaguely familiar. I think you are right and that it's directly related to the data uri format. Can you try with an external js file and see if it works? Once we are sue it's the data uri, we might be able to tweak things to get it to work on all browsers (without resulting to an external file).

@likangning93
Copy link
Contributor Author

Can you try with an external js file and see if it works?

Seems to work fine with an external file, here on deployed Sandcastle.

@mramato
Copy link
Contributor

mramato commented Aug 31, 2018

@shunter do you remember something about data uris being cross origin on IE 11 or anything odd about data uris in IE11 in general?

@shunter
Copy link
Contributor

shunter commented Aug 31, 2018

The data URL in the example is faulty because it contains spaces and newlines, etc. The result is that it bypasses the data URL detection in Resource.js. URLs must be encoded correctly.

And yes, data URLs are not same-origin with anything in some browsers when loaded via XHR, which is why I added that special detection and handling in #1533 in the first place.

@mramato
Copy link
Contributor

mramato commented Aug 31, 2018

Cool thanks, shame on my for not looking at the sample code better.

@likangning93 likangning93 force-pushed the additionalProjections branch from 43131e1 to 187963c Compare August 31, 2018 19:07
@likangning93 likangning93 force-pushed the additionalProjections branch from 187963c to 358308b Compare August 31, 2018 19:08
@likangning93
Copy link
Contributor Author

shame on my for not looking at the sample code better.

or, shame on me for not knowing how to URI.
I still don't know how to URI, but this works in IE now so I'm going to check the box.

@pjcozzi
Copy link
Contributor

pjcozzi commented Sep 10, 2018

What to do about projections that split the globe somewhere other than the antimeridian?

and

what do weird projections do to terrain LOD selection? (see EPSG 3411 below)

Please scope this accordingly based on customer interest, e.g., it is OK to not support these or not support them well if initial customers are OK with it. Then roadmap them.

Copy link
Contributor

@pjcozzi pjcozzi left a comment

Choose a reason for hiding this comment

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

  • I mentioned this in another PR but proj4.js is 169 KB. What does it gzip to? When does Cesium load it, e.g., only when requested with a web worker? Are we OK with the size?
  • I did not get a chance to run this. Please please please profile representative cases.

}

// Add lat/long points
for (var x = -175; x < 180; x += 10) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor, but would call these locals lon and lat since that is what they are.


var projectionText =
'function projectionFactory(callback) {\n' +
' function project(longitude, latitude, height, result) {\n' +
Copy link
Contributor

Choose a reason for hiding this comment

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

Here and below have to use scalars and arrays due to the web workers, right? They can't use Cesium's cartesian and cartographic types?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not due to web workers, I just think it's more flexible to use something built into the language when interfacing. Users might pull in other vector libraries, so imposing Cesium types didn't seem appropriate. It might even create confusion, like give an impression that all Cesium types are accessible from the callback out-of-the-box (they aren't).

I also wonder if that could mess with callbacks written in Typescript, but I don't know enough about that to say for sure.

Copy link
Contributor

Choose a reason for hiding this comment

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

I dunno - we use Cesium types throughout Cesium for interfaces that we implement and/or interfaces our users implement. Seems arbitrary to make this different.

}

// Add lat/long points
for (var x = -175; x < 180; x += 10) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment. Can also remove the code comment on the line above.

CustomProjection.prototype.unproject = function(cartesian, result) {
//>>includeStart('debug', pragmas.debug);
if (!this._ready) {
throw new DeveloperError('CustomProjection is not loaded. User CustomProjection.readyPromise or waith for CustomProjection.ready to be true.');
Copy link
Contributor

Choose a reason for hiding this comment

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

waith

"wait?"

var deferred = when.defer();
var buildPlugin;
(function() {
eval(scriptText + 'buildPlugin = ' + functionName + ';');
Copy link
Contributor

Choose a reason for hiding this comment

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

(1) we're sure about this? and (2) this didn't require a jsHint workaround?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This didn't require a jsHint workaround, and our eslint doesn't disallow eval because it was "likely never to come up".

This could use more thought though. @mramato, @shunter, can you guys weigh in a bit here?
Just as a reminder, the goal is for users to provide their own functions for project and unproject, and they may be pulling in other libraries to implement those. project and unproject also have to function on web workers.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm still confused why they can't just implement a class with these functions.

Also, check out three.js for its delayed module loading architecture. Could be useful here and at a potentially larger scale in the future.

Copy link
Member

Choose a reason for hiding this comment

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

FWIW, we use Cesium in at least one app with a content security policy that doesn't allow use of eval, and I suspect a lot of others do as well. If the automatic reprojection breaks in that environment, it's not a disaster (for us, cause we already have a reprojection mechanism), but if Cesium fails to load because if it, that's bad.

Copy link
Member

Choose a reason for hiding this comment

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

Do you maybe just want to use JSONP or something here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

RequireJS allows loading files after page load: https://requirejs.org/docs/api.html#afterload

I have this proof-of-concepted, but it needs another set of eyes from someone who knows more about how module systems work and how this will interact with built Cesium, @mramato are you the right person to ask?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also @kring I'm going to give jsonp a try as you originally recommended.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The RequireJS route doesn't work for built Cesium.

JSONP works after some possible Resource bugfixes but I don't have it working on web workers yet because of the dependence on window. Maybe we can work around that using importScripts somehow.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's not really the reliance on Window that's the problem, it's the fact that document isn't available. Ideally it would be nice to fix Resource so that it works in all cases (this could even be done in a separate PR for easier testing/review)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I might need a lot more guidance to overhaul Resource, so just to keep things moving I've updated for now with JSONP and the importScripts workaround. This doesn't work with data URIs on IE11, but that doesn't seem like a huge deal. I updated Sandcastle demo to load the same "custom projection" from a file when IE11 is detected.

@kring thanks for the JSONP suggestion, sorry I ever doubted you!

}
});

var projectionArray = [0, 0];
Copy link
Contributor

Choose a reason for hiding this comment

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

Explicitly include "scratch" in the name.

}

// without clamp proj4 might crash
projectionArray[0] = CesiumMath.clamp(CesiumMath.toDegrees(cartographic.longitude), -180 + CesiumMath.EPSILON7, 180 - CesiumMath.EPSILON7);
Copy link
Contributor

Choose a reason for hiding this comment

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

Here and below, include .0 when the value is intended to be floating point.

} catch(e) {
if (!this._forwardFailed) {
// Log a warning the first time a projection fails
console.warn('proj4js forward failed for ' + projectionArray + ' with projection ' + this._wkt);
Copy link
Contributor

Choose a reason for hiding this comment

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

Here and below, are we sure console.warn is the right approach compared to throwing an exception? How often is console.warn used elsewhere in Cesium - and for what?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately Proj4js frequently fails when unprojecting positions in EPSG:2039, even when they appear to be in-bounds. It may also fail for other projections if camera navigation moves outside bounds, which we must assume will happen for Columbus View.

I'll change this to oneTimeWarning, which we use in other places.

if (serializedMapProjection.isMercator) {
projection = new WebMercatorProjection(ellipsoid);
}
if (serializedMapProjection.isGeographic) {
Copy link
Contributor

Choose a reason for hiding this comment

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

else?

Can both isMercator and isGeographic be true at the same time? Perhaps a local enum is better.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They're false at the same time for Custom and Proj4js projections. I'll switch to an enum though, that makes sense.

* The terrain mesh should contain projected positions for 2D space.
* @type {Boolean}
*/
this.has2dPositions = defined(center2D);
Copy link
Contributor

Choose a reason for hiding this comment

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

This naming convention is not consistent with center2D, which is not consistent with Cesium's use of camelCase. Maybe this is a special case. I dunno. @lilleyse

Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment throughout.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll switch to hasPositions2D

@likangning93
Copy link
Contributor Author

I mentioned this in another PR but proj4.js is 169 KB. What does it gzip to? When does Cesium load it, e.g., only when requested with a web worker? Are we OK with the size?

Still need to answer these questions, but technically we could remove built-in Proj4js support and just demonstrate in Sandcastle or via a blog post how users who want EPSGs and whatnot can wire Proj4js up via CustomProjection. I just have to find a better way for that to work first...

@likangning93
Copy link
Contributor Author

I mentioned this in another PR but proj4.js is 169 KB. What does it gzip to? When does Cesium load it, e.g., only when requested with a web worker? Are we OK with the size?

Still need to answer these questions, but technically we could remove built-in Proj4js support and just demonstrate in Sandcastle or via a blog post how users who want EPSGs and whatnot can wire Proj4js up via CustomProjection. I just have to find a better way for that to work first...

I think we should be ok on size, it's smaller than our Model.js, and also seems to compress pretty well. Gzip alone can get it down to around 45 kb, and it looks like it also uglifies pretty well - gzip + uglify is about 25 kb.

Also I don't think we can get it to load on request like Draco, it has to be available on the main thread at app startup. Perhaps when we overhaul our module system.

@pjcozzi
Copy link
Contributor

pjcozzi commented Sep 15, 2018

I think we should be ok on size, it's smaller than our Model.js, and also seems to compress pretty well. Gzip alone can get it down to around 45 kb, and it looks like it also uglifies pretty well - gzip + uglify is about 25 kb.

Doesn't sound great, what % increase is that for Cesium.js? To benefit what % of CesiumJS users? Maybe cleaning it up can just go on the roadmap, but I don't think we can rationale a size increase like this as best practice.

@pjcozzi
Copy link
Contributor

pjcozzi commented Sep 15, 2018

Did you look at the three.js module / runtime loading system?

@kring
Copy link
Member

kring commented Sep 16, 2018

Did you look at the three.js module / runtime loading system?

@pjcozzi do you happen to have a reference handy for this? I found:
https://threejs.org/docs/index.html#manual/en/introduction/Import-via-modules
But that's not really meaningfully different from what you guys are already doing with your npm package. Is there some other fancy thing going on in three.js land that I missed in my quick googling?

@pjcozzi
Copy link
Contributor

pjcozzi commented Sep 17, 2018

Did you look at the three.js module / runtime loading system?

@pjcozzi do you happen to have a reference handy for this? I found:
https://threejs.org/docs/index.html#manual/en/introduction/Import-via-modules
But that's not really meaningfully different from what you guys are already doing with your npm package. Is there some other fancy thing going on in three.js land that I missed in my quick googling?

@donmccurdy any chance you could point us to three.js' module system?

@donmccurdy
Copy link

donmccurdy commented Sep 17, 2018

three.js uses less of a system than a pattern — the core library is small (mainly just the parts that everything else depends on) and features that can be extracted are generally left for the user to include (or not) in their build. Simple example using <script/> tags:

<script src="node_modules/three/build/three.min.js"></script>
<script src="node_modules/three/examples/js/loaders/GLTFLoader.js"></script>

We have ways to make this work nicely with CommonJS modules, ES6 modules, and NPM distribution, as well. In certain cases the core library will log warnings or throw errors to let users know a necessary module is missing, but generally the core library does not depend on anything else.

Not sure if this answers your question, or if I've misunderstood the comments above. I can only think of one case where we do runtime script loading; in general that is not very compatible with npm distribution and build tools. TurfJS (https://github.com/Turfjs/turf) has a nice system, too — see their packages/* folder on GitHub.

@likangning93
Copy link
Contributor Author

Are polygons on terrain projected correctly?

welltheresurproblem

Red indicates region of the polygon that has "rectangle fragment culling," added in #6393

@likangning93
Copy link
Contributor Author

likangning93 commented Oct 12, 2018

Are polygons on terrain projected correctly?
EDIT: Also, should the textures be that different?

This is actually a pretty significant problem, the materials-on-ground-primitives code for 2D assumes an equirectangular projection for everything which is completely untrue here and would have been wrong at large scales for scenes using the WebMercatorProjection.

Basically, materials on GroundPrimitives in 2D work by comparing the distance from each fragment to a pair of orthogonal reference planes that line up with latitude/longitude. This comparison allows for texture coordinates as well as fragment culling so non-overlapping ground primitives won't interfere with each other.

However, non-equirectangular projections will need stretched or curved planes for this to continue working properly. Yikes!

[EDIT] a couple options:
We may be able to mostly fix this by making the bounding Rectangles around GroundPrimitives used to compute planes, etc. larger, but texture coordinates won't curve the same way that they would for regular Primitives in something like the mollweide projection.

Another solution is to redesign GroundPrimitives so that each Polygon in a GroundPrimitive is actually a series of quad volumes and each quad volume has vertex attributes specifying its planes and whatnot, but I think @bagnell and I discussed this at some point for other reasons and thought it would be nuts. Either way, an experiment in this seems out of scope for this PR.

Option 3 is to say that materials on GroundPrimitives aren't compatible with alternate projections, but that seems lame.

@likangning93
Copy link
Contributor Author

We may be able to mostly fix this by making the bounding Rectangles around GroundPrimitives used to compute planes, etc. larger, but texture coordinates won't curve the same way that they would for regular Primitives in something like the mollweide projection.

The documentation does specify that materials on GroundPrimitives should only be used notationally, though, so I'm going to try to prove this approach out first.

@likangning93
Copy link
Contributor Author

*wraparound in 2D broken for many projections

Maybe test if they're rectangular by projecting the corner points and taking the dot product of the connected edges. If the dot product is 0 for each corner then it's rectangular. If it's not rectangular, use the option that doesn't have the infinite horizontal scroll in 2D.

I spent some time prodding 2D/CV camera stuff, added a limiter to Proj4Projection as well so we don't have to have cameras that have infinite bounds with things like local transverse mercator, and then some more time playing with a couple more projections and trying to figure out where the infinite scroll code was breaking.

A few somewhat late observations about infinite scrolling in 2D:

  • doesn't make a lot of sense for projections that cover less than -180 to 180
  • possibly only makes sense for east-west cylindrical projections?
  • currently only works for projections that are symmetrical about the prime meridian

Funny thing, right now infinite scroll will "work" for something like mollweide or Mercator limited to -160, 160, it just won't make a lot of sense. But it's pretty broken for polar projections.

@bagnell the dot-product check you proposed will probably work for catching most cases, but maybe instead should we just default to MapMode2D.ROTATE when using proj4 and custom projections, and then write in the documentation that MapMode2D.INFINITE_SCROLL + weird projections is a "use-at-your-own-risk" sort of thing?

@likangning93
Copy link
Contributor Author

maybe instead should we just default to MapMode2D.ROTATE when using proj4 and custom projections, and then write in the documentation that MapMode2D.INFINITE_SCROLL + weird projections is a "use-at-your-own-risk" sort of thing

But alas, zooming in and out of a polar projection in scene mode 2D, with rotation enabled, leads to wild rotation during the zoom. Here on Sandcastle. That's not good.

@likangning93
Copy link
Contributor Author

likangning93 commented Oct 18, 2018

If you open the proj4js example, zoom in and use the middle click to rotate around a point. It always rotates about the center of the screen no matter where you click (with and without terrain).

I think this is fixed.

But alas, zooming in and out of a polar projection in scene mode 2D, with rotation enabled, leads to wild rotation during the zoom. Here on Sandcastle. That's not good.

Mostly fixed, biggest problem was that a lot of the heading camera code assumes a normal-rectangular projection, where heading generally means a similar 2D vector in various places on the map.
Updated so heading given a location and a 2D vector is approximated, which isn't perfect but at least won't be as whirly anymore in Polar.

Also made this Sandcastle to demonstrate that setting heading works as expected (also for fun).

Are polygons on terrain projected correctly?

Mostly fixed, using larger bounding rectangles. Texture coordinates won't curve though, so the same polygon on the globe or in Geographic 2D/CV will look quite different vs. in mollweide or polar.

@likangning93
Copy link
Contributor Author

projection/unprojection sometimes fails, currently caught and logged the first time, then silent

Do you have an example?

I used to run into this a lot with EPSG 2039, but I can't reproduce it anymore in this branch. Maybe it went away in Proj4 2.5.0. Here's a Sandcastle that used to run into failures pretty consistently, though.

@likangning93
Copy link
Contributor Author

What to do about projections that split the globe somewhere other than the antimeridian?

Do we plan to support projections with multiple splits? Or is that for the future?

For now let's say that we won't support projections with weird splits or multiple splits. Most "local area" EPSGs are split-free in their defined region anyway, and I think users who want, say, Dymaxion projections and stuff should have enough flexibility in CustomProjection to hack in something like multiple splits. Maybe something like:

  • create a margin along the seams in projection code
  • drop height for terrain vertices in the seam margins
  • add a giant black Entity below ground to mask out the drop
  • add app entities so they are pre-split across seams

I kind of want to try this now...

@likangning93
Copy link
Contributor Author

@bagnell sorry for the delayed response, but I think we're ready for another look.

@likangning93
Copy link
Contributor Author

@bagnell we're also going to shift goals so this will become a base branch for "projection support" in general. Still experimenting with imagery too.

@pjcozzi
Copy link
Contributor

pjcozzi commented Oct 28, 2018

@bagnell can you please review this?

@bagnell
Copy link
Contributor

bagnell commented Oct 30, 2018

The only comment I have is when zooming in 2D and the heading changes. Instead, can you keep the current up vector. Otherwise, this looks good to me.

@likangning93 likangning93 changed the base branch from master to projections October 31, 2018 15:35
@likangning93
Copy link
Contributor Author

The only comment I have is when zooming in 2D and the heading changes. Instead, can you keep the current up vector.

@bagnell should be fixed!

@bagnell
Copy link
Contributor

bagnell commented Oct 31, 2018

This looks good to me. Have you done any performance tests?

@likangning93
Copy link
Contributor Author

This looks good to me. Have you done any performance tests?

Knew I was missing something...

@likangning93
Copy link
Contributor Author

Performance

Disclaimer that all numbers are kind of unscientific, but TL;DR performance doesn't seem to be impacted very much.

All projections used mollweide ESRI:53009 +proj=moll +lon_0=0 +x_0=0 +y_0=0 +a=6371000 +b=6371000 +units=m +no_defs.

FPS

I wanted to see if the modified shader path/vertex attributes would impact runtime performance, but this is hard to see on most modern, mid-range computers. So I ran on a very low-power Intel machine on hand, and tested web mercator via Proj4 vs. Cesium's built-in Web Mercator, using Natural Earth in both cases. Testing was done at 1366x768. Here's what the code looked like. These numbers were the same for both in "home" views in different scenemodes:

scenemode: 3D CV 2D
fps: 18-19 20-21 9-10

Tile projection time

I created a branch here: https://github.com/likangning93/cesium/tree/additionalProjectionsPerf
This restricts Heightmap-type terrain tile processing to a single concurrent worker and adds some timing code. There's a consistent performance difference between tiles that use Proj4 projection, but it seems to mostly be dwarfed by the total "tile creation" time including (I think?) worker communication.
All times derived from about 1000 projections, which I got by zooming around the globe a bit. I threw out the first 100 timings in each case b/c these included worker spinup. All times in milliseconds using performance.now().

Celeron N2920:

projection method proj4 geographic
avg time "create mesh" 12.341 11.974
time on worker 2.607 1.545

i7 4980HQ:

projection method proj4 geographic
avg time "create mesh" 1.423 1.430
time on worker 0.542 0.309

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.

8 participants