-
Notifications
You must be signed in to change notification settings - Fork 161
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
Merge and adapt Jason Davies’ clip-polygon branch from d3v3 to v4 #108
Conversation
Original code: https://github.com/d3/d3/commits/clip-polygon See #46 and d3/d3-geo-projection#86 (comment) I went back to the original naming: - clip/polygon.js defines clipPolygon() - clip/rejoin.js defines clipPolygonRejoin() A test block: https://bl.ocks.org/Fil/cb7930254c9e7062114678d62d9be5ac Documentation, tests and examples of projections will come at a later stage. Thanks to: - Jason Davies @jasondavies for the genius algorithm and implementation - Mike Bostock @mbostock for the explanations and guidance - Enrico Spinielli @espinielli for the Furuti and Cahill-Keyes projections
There is still a weird issue: sometimes a part of the drawing will not be rendered. In https://bl.ocks.org/Fil/cb7930254c9e7062114678d62d9be5ac, the outline of the Sphere will sometimes be half cut (varying between reloads). In https://bl.ocks.org/Fil/a5c101787b70c93ef2455159f9fd26cb the whole world will either be totally drawn, or totally blank. |
I have a vague recollection of a discussion with Jason regarding resorting the points along the polygon edge after clipping… I wonder if that is the issue here. Sorry, just speculation. |
I just cloned the Furuti3 and made it minimalistic in order to better see what the issue is with the cuts... Refresh a few times to get at least one of both... |
[EDIT: OBSOLETE COMMENT] I think I might have found the cause in It looks like the issue was caused by the sphere being clipped by the outline — which also represents the sphere, except it's computed by joining the faces not by running sphereStream. So we had two "almost identical" polygons, and some random crept in when comparing epsilons. Having a "clipPolygon sphere" using a much smaller epsilon than the sphereStream seems to solve it (or, almost solve it), in that the clipPolygon contains the sphereStream. Not 100% sure yet though. |
OK the solution is much simpler than playing with epsilons. It's only a case of not clipping the sphereStream. I have pushed the code to http://blockbuilder.org/Fil/c36ed66a4d50d77150712c80642a78d5 and http://blockbuilder.org/Fil/cb7930254c9e7062114678d62d9be5ac and both seem 100% solved. the solution is: save the clipPolygon(), remove it (with clipAngle 180), stream the sphere, then set the clipPolygon again — just like we do for rotate().
I'll push it to the PR tomorrow. Good night! |
Looking forward to trying it out! |
I was playing with So this bl.ock I found a couple of surprises, and only (sigh!) one solution.
Maybe it is related to clipping on |
For [1] it appears to be one of the bugs in polygonContains (#105 (comment)). For [2] the polyhedral Sphere w/ clipPolygon, something is still missing. I had a similar issue with Waterman for example. In this case it seems we can solve it by introducing |
Thank you now I got JvW's foldout: |
I pushed clipNone and a sort of API to call it with However we still have a problem to solve: if we use world-atlas/110m or 50m, and use rotate([0,0]), clipPolygon fails totally. I made a test bed to show this here: Strangely world-110m in Enrico's block doesn't have this problem. So I think we're close (but maybe we just opened a new can of worms!). Note that when this is solved (currently, when using a small rotation), we can also dispense with the SVG or canvas clipping and use d3-geo clipPolygon instead: progress! |
The most visible difference between the topojson files is that, in Enrico's world-110m, mainland Russia is made of two polygons (one East of the antimeridian line, one West); in the case of world-atlas, it's one polygon that wraps around. |
Opppss! Where did I get world110m from? maybe it is one of the older versions... |
I think I'm done here; the question is what API design. Hacking clipPolygon is not a breaking change (everything still works the same in d3-geo-projection.v2) but it's certainly worth a version change so that d3-geo-projection can require it when we introduce new polyhedral projections. |
My ideal API would be projection.clip(clip) where clip is an object so some variety that controls the clipping method (implementation) and any parameters. So for example: projection.clip(d3.geoClipAntimeridian); // replaces projection.clipAngle(null)
projection.clip(d3.geoClipCircle(90)); // replaces projection.clipAngle(90)
projection.clip(d3.geoClipPolygon(…)); // replaces projection.clipPolygon(…)
projection.clip(null); // disables clipping entirely? With the new “pipeline” design I was hoping to have clipping be extensible, but I think it’d be fine to punt on that extensibility for now. So in other words only a fixed number of clip implementations would be supported (none, antimeridian, circle, polygon), and if you tried to pass in an instance of something else it would throw an error. And likewise the clip instance wouldn’t have any public methods, other than maybe some way of inspecting what it represented. |
+1 for keeping whatever API private for now, there are still dark corners in this area that need to be cleaned. Also, currently pre-clipping is entangled with rotation, with no obviously compelling reason (the examples given are now all running smoothly with or without #90), so any API that we expose here would be quite complicated and possibly subject to change. Would In the first case, we could have |
The most opaque way of doing it would be to just have a sentinel object. Something equivalent to: var clipAntimeridian = {}; It’s a little different for the circle class, since the angle is configurable. So you might say similar to: function ClipCircle(angle) {
this.angle = angle;
} And then in the d3-geo projection class, you’d say something like: if (clip === clipAntimeridian) {
doAntimeridianClipping();
} else if (clip instanceof ClipCircle) {
doCircleClipping(clip.angle);
} else {
etc();
} You could do this with an object and a type string, but generally I prefer to use symbols as identifiers rather than strings. |
OTOH, if we do decouple clipping from rotation, it’s tempting to make the clipping interface implement the projection stream interface—then you can pass in whatever implementation you want to clipping, and we don’t have to have a fixed set of implementations. (Note that this would still be less flexible than the imagined projection pipeline, as you’d only be able to control one part of a fixed pipeline.) The downside of this approach would be that the clipping projection stream operates on coordinates in radians, which is inconsistent with the rest of the API. And I think switching it to operate in degrees for consistency would have a noticeable performance and code complexity cost… though, I haven’t looked into it much. |
I feel this is taking us too far away from the original goal of this PR. The future stream/pipeline API is going to be a different and also very big effort — and it's only tangentially related to this. I'd prefer to land this small win now before opening the next. Can we maybe just add It's not perfect, and I still don't like the logic in the setup of the Sphere stream in polyhedral https://github.com/Fil/d3-geo-projection/blob/clipPolygon/src/polyhedral/index.js#L105 — also I don't understand why |
I have doubts as to whether the proposed pipeline API will ever happen—it was an experiment from several years ago and I don’t currently have the bandwidth to revisit it. I’m not proposing we take that on now, and certainly not as part of this effort. What I was proposing was in #108 (comment) doesn’t seem too onerous to do now, and is cleaner than overloading clipAngle with -1, no? |
I'll test that tomorrow with the various projections. (And now I think I understand why polyhedralCollignon fails with clipPolygon, and it's not related to clipPolygon itself, but to the way polyhedral builds the sphere.) |
…way polyhedral constructs its sphere
This morning I researched the issue with the way the Sphere stream is set up in polyhedral, and found a way to fix it in polyhedral so that If this holds, we won't need So let's put this to rest & do more tests for a while. My current tests are listed at https://bl.ocks.org/Fil/fc967d8ac6aa246863c6f2a4b7dbe41c |
… centroids — solves a lot of the issues at d3/d3-geo#108 (comment) and allows to use clipPolygon() properly on all polyhedral projections (tested to this date).
Thanks for your perseverance with this. Your tests are beautiful! I’m happy with just adding projection.clipPolygon. As I understand it, there would still be three states:
I think it might be cleaner to introduce a second new method, projection.clipAntimeridian, and use that to enable antimeridian cutting rather than passing null to projection.clipAngle. (Otherwise, we’d presumably need to allow passing null to projection.clipPolygon as a redundant way of enabling antimeridian cutting?) The projection.clipAntimeridian method would accept and return a boolean, and we’d deprecate allowing projection.clipAngle to receive null. |
Is anyone willing to recreate Jason Davies’ approximation of Fuller’s cuboctahedron projection? |
Will do — in fact projection.clipPolygon() already accepts null (or equivalently []), but in the current implementation it switches to clipNone rather than clipAntimeridian (but that's because the real clipping is done by geoPolyhedral). My use case was to have a full graticule on Lee's projection — it's tricky because clipPolygon will remove the 180° meridian. But several alternatives can solve it: |
Fuller's historical projection(s) are an interesting challenge:
It's probably not the scope of d3-geo-projection to reconstruct the major historical projections, but trying to do so is going to be very interesting, even for projections that are now considered obsolete. A discussion for d3-geo-projection, or maybe a separate project altogether? |
You can get the poster images of Mar 1943 Life's Dymaxion map from Google books, see pag 44 and onwards. Yes reproducing these "esoteric" maps would be fun. (now that |
Given the icosahedron's faces, Jason's Airocean layout can be obtained by splitting a couple of faces; something like: // Split relevant face at centroid. (index depends on how you build your icosahedron)
var face = icosahedron[6],
centroid = face.centroid;
temp = face.slice();
face[0] = centroid;
icosahedron.push(face = [temp[0], centroid, temp[2]]);
face.centroid = centroid;
icosahedron.push(face = [temp[0], temp[1], centroid]);
face.centroid = centroid;
// Split the other face at middle point of edge.
face = icosahedron[8];
temp = face.slice();
centroid = d3.geo.interpolate(face[1], face[2])(.5);
face[1] = centroid;
icosahedron.push(face = [temp[0], temp[1], centroid]);
face.centroid = icosahedron[8].centroid;
icosahedron[7].splice(2, 0, centroid); And then build/modify your spanning tree accordingly... |
- projection.clipAntimeridian(), getter, returns (clip == Antimeridian) - projection.clipAntimeridian(true-ish), setter, returns projection, sets clip = Antimeridian - projection.clipAntimeridian(false-ish), setter, returns projection, does not change clip strategy (???) - projection.clipAngle(), getter, returns theta or null - projection.clipAngle([theta]), setter, returns projection, sets clip = Circle with angle theta - projection.clipAngle(false-ish), setter, returns projection, sets clip = Antimeridian (backward compatibility) - projection.clipPolygon(), getter, returns polygon or null - projection.clipPolygon([polygon]), setter, returns projection, sets clip = Polygon with that polygon - projection.clipPolygon(false-ish), setter, returns projection, sets clip = NoClip (???) Add a few basic tests including one for #54.
API updated, see the question marks in 6f09e3e if you have some patience for it |
What if clipAntimeridian(false) behaved like clipPolygon(false), in other words adopting the clipNone behavior. Then only clipAngle(false) would be the weird one for backwards-compatibility, and in a future major release we could make clipAngle(false) also trigger clipNone instead of clipAntimeridian? |
Pushed clipAntimeridian(false) -> clipNone.
I feel more conservative about backwards compat and would prefer not to
break anyone's map without a strong case.
Also, if in a future release clipAngle(0) should change, it would probably
make more sense as a limit case of clipAngle(x) where x -> 0 (i.e. a
vanishing map https://bl.ocks.org/Fil/a17dd08e538716fb49a6760384b9ae51 )
rather than clipNone.
|
Another (last?) API change for consistency: clipPolygon now reads and returns a GeoJSON object. |
this branch https://github.com/Fil/d3-geo-projection/tree/clipPolygon%2Binterrupted makes sure that the clipPolygon() can work with all of the “interrupted” projections. If it didn't work it would have been a red flag. For sure it is slower than clipAntimeridian (so maybe it should not be merged, or made into an option), and there are a few hiccups that will need to be investigated. Sometimes Antarctica "escapes" the clipping and bleeds all over the map. And bits of the graticule are discarded (apparently when they fall exactly on one of the clipPolygon's vertices). |
little attempt at (pseudo) Airocean (bl.ock), no land rotation yet... ...but I get artefacts Any hints/helps is welcome ;-) |
http://blockbuilder.org/Fil/6d2fe81f148aefb04dd1c1792068ff93
|
you are an ace! |
This allows arbitrary implementations of spherical clipping and pixel clipping to be specified on a projection. We’re still not achieving the generality of a composable projection pipeline, but at least we can plug in new implementations, such as the forthcoming spherical polygon clipping (#108).
This is now living in https://github.com/d3/d3-geo-polygon ; let's discuss it there, beginning with some examples! |
Note that d3-geo-polygon is still unstable. When we stream points that belong to the clipping polygon itself, it sometimes bleeds out -- for example on the Waterman projection with a default aspect -- rotate [0,0]). I have been as conservative as possible: d3-geo-projection works as usual without d3-geo-polygon, and checks that it has projection.preclip (d3-geo > 1.8.1). Examples and issues at [d3-geo-polygon](https://github.com/d3/d3-geo-polygon) Solves #129 Solves #124 Solves d3/d3-geo#108 Solves #86
Note that d3-geo-polygon is still unstable. When we stream points that belong to the clipping polygon itself, it sometimes bleeds out -- for example on the Waterman projection with a default aspect -- rotate [0,0]). I have been as conservative as possible: d3-geo-projection works as usual without d3-geo-polygon, and checks that it has projection.preclip (d3-geo > 1.8.1). Examples and issues at [d3-geo-polygon](https://github.com/d3/d3-geo-polygon) Solves #129 Solves #124 Solves d3/d3-geo#108 Solves #86
Fixes #46