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

[WIP] Spherical triangulation and interpolation #182

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Changes from 7 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e40fff5
Implement spherical triangulation
asinghvi17 Jul 21, 2024
ef028e4
Use DelaunayTriangulation's API to get better results
asinghvi17 Jul 21, 2024
27a7124
North pole is at infinity if I use wikipedia (which I do)
asinghvi17 Jul 21, 2024
0476e3e
visualize the transform etc for better understanding
asinghvi17 Jul 21, 2024
c6da37f
Working spherical triangulation function
asinghvi17 Jul 21, 2024
1bcba2c
Plot correctly
asinghvi17 Jul 21, 2024
733faac
First shot at hacking NaturalNeighbours interpolation
asinghvi17 Jul 21, 2024
6979ce4
Update docs/src/experiments/spherical_delaunay_stereographic.jl
asinghvi17 Jul 21, 2024
5f1c6bb
Remove degenerate faces
asinghvi17 Jul 22, 2024
72c395c
Use a bit more DelTri API
asinghvi17 Jul 23, 2024
f1ffce7
Add a Quickhull.jl method for 3d triangulation
asinghvi17 Jul 26, 2024
41a3e2e
Move all coordinate transforms on top
asinghvi17 Jul 26, 2024
893b647
add another minimal line + test for Quickhull
asinghvi17 Jul 26, 2024
ecd7996
Add license and more description
asinghvi17 Jul 26, 2024
7e83107
First buggy implementation of Laplace interpolation
asinghvi17 Jul 28, 2024
937e66f
Fix incorrect z indexing
asinghvi17 Jul 28, 2024
2f6d4f2
Merge branch 'main' into as/stereographic_deltri
asinghvi17 Jul 28, 2024
0a7a4e6
More correctness
asinghvi17 Jul 28, 2024
afe40f5
Merge branch 'as/stereographic_deltri' of https://github.com/JuliaGeo…
asinghvi17 Jul 28, 2024
37190f7
Experiment: why is my Bowyer-Watson wrong
asinghvi17 Jul 28, 2024
7a3667e
Reset `edge_reoccurs`
asinghvi17 Jul 28, 2024
bd608e7
Fix ambiguity
asinghvi17 Jul 28, 2024
e638b17
Correct the winding order
asinghvi17 Jul 28, 2024
4de0db7
Refactor files into a core spherical triangulation file + experiments
asinghvi17 Jul 29, 2024
cf3465c
Make the natural neighbour interpolation file natively runnable
asinghvi17 Jul 29, 2024
3f36634
More fixes for point order from Bowyer Watson
asinghvi17 Jul 30, 2024
6b33e28
Get the file working again
asinghvi17 Jul 30, 2024
46341c1
Fix some bugs
asinghvi17 Jul 31, 2024
f8fd202
Add a minimal test
asinghvi17 Jul 31, 2024
a63c342
add spherical segmentizer
asinghvi17 Aug 3, 2024
e23c419
Make the `_segmentize` function more generic
asinghvi17 Aug 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
332 changes: 332 additions & 0 deletions docs/src/experiments/spherical_delaunay_stereographic.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
#=
asinghvi17 marked this conversation as resolved.
Show resolved Hide resolved
# Spherical Delaunay triangulation using stereographic projections

This is the approach which d3-geo-voronoi and friends use. The alternative is STRIPACK which computes in 3D on the sphere.
The 3D approach is basically that the 3d convex hull of a set of points on the sphere is its Delaunay triangulation.
=#

import GeometryOps as GO, GeoInterface as GI, GeoFormatTypes as GFT
import Proj # for easy stereographic projection - TODO implement in Julia
import DelaunayTriangulation as DelTri # Delaunay triangulation on the 2d plane
import CoordinateTransformations, Rotations

using Downloads # does what it says on the tin
using JSON3 # to load data
using CairoMakie, GeoMakie # for plotting
import Makie: Point3d

function delaunay_triangulate_spherical(input_points; facetype = CairoMakie.GeometryBasics.TriangleFace)
# @assert GI.crstrait(first(input_points)) isa GI.AbstractGeographicCRSTrait
points = GO.tuples(input_points)

pivot_ind = findfirst(x -> all(isfinite, x), points)

pivot_point = points[pivot_ind]
necessary_rotation = #=Rotations.RotY(-π) *=# Rotations.RotY(-deg2rad(90-pivot_point[2])) * Rotations.RotZ(-deg2rad(pivot_point[1]))

net_transformation_to_corrected_cartesian = CoordinateTransformations.LinearMap(necessary_rotation) ∘ UnitCartesianFromGeographic()
stereographic_points = (StereographicFromCartesian() ∘ net_transformation_to_corrected_cartesian).(points)
triangulation_points = copy(stereographic_points)

# Iterate through the points and find infinite/invalid points
max_radius = 1
really_far_idxs = Int[]
for (i, point) in enumerate(stereographic_points)
m = hypot(point[1], point[2])
if !isfinite(m) || m > 1e32
push!(really_far_idxs, i)
elseif m > max_radius
max_radius = m
end
end
@debug max_radius length(really_far_idxs)
far_value = 1e6 * sqrt(max_radius)

if !isempty(really_far_idxs)
triangulation_points[really_far_idxs] .= (Point2(far_value, 0.0),)
end

boundary_points = reverse([
(-far_value, -far_value),
(-far_value, far_value),
(far_value, far_value),
(far_value, -far_value),
(-far_value, -far_value),
])
Copy link
Contributor

Choose a reason for hiding this comment

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

This is unused. Were there issues? You mentioned an issue with convert_boundary_points_to_indices, is it related?

Copy link
Contributor

Choose a reason for hiding this comment

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

boundary_faces is also unused

Copy link
Member Author

Choose a reason for hiding this comment

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

The issue here was that a lot of faces were connecting to two boundary points and one data point, which is effectively a degenerate face and thus useless.


# boundary_nodes, pts = DelTri.convert_boundary_points_to_indices(boundary_points; existing_points=triangulation_points)

# triangulation = DelTri.triangulate(triangulation_points; boundary_nodes)
triangulation = DelTri.triangulate(triangulation_points)
# for diagnostics, try `fig, ax, sc = triplot(triangulation, show_constrained_edges=true, show_convex_hull=true)`

# First, get all the "solid" faces, ie, faces not attached to boundary nodes
boundary_faces = Iterators.filter(Base.Fix1(DelTri.is_boundary_triangle, triangulation), DelTri.each_solid_triangle(triangulation)) |> collect
faces = map(facetype, DelTri.each_solid_triangle(triangulation))

for ghost_face in DelTri.each_ghost_triangle(triangulation)
push!(faces, facetype(map(ghost_face) do i; i ≤ 0 ? pivot_ind : i end))
asinghvi17 marked this conversation as resolved.
Show resolved Hide resolved
end

return faces
end

# These points are known to be good points, i.e., lon, lat, alt
points = Point3{Float64}.(JSON3.read(read(Downloads.download("https://gist.githubusercontent.com/Fil/6bc12c535edc3602813a6ef2d1c73891/raw/3ae88bf307e740ddc020303ea95d7d2ecdec0d19/points.json"), String)))
faces = delaunay_triangulate_spherical(points)

# This is the super-cool scrollable 3D globe (though it's a bit deformed... :D)
f, a, p = Makie.mesh(map(UnitCartesianFromGeographic(), points), faces; color = last.(points), colormap = Reverse(:RdBu), colorrange = (-20, 40), shading = NoShading)

# We can also replicate the observable notebook almost exactly (just missing ExactPredicates):
f, a, p = Makie.mesh(points, faces; axis = (; type = GeoAxis, dest = "+proj=bertin1953 +lon_0=-16.5 +lat_0=-42 +x_0=7.93 +y_0=0.09"), color = last.(points), colormap = Reverse(:RdBu), colorrange = (-20, 40), shading = NoShading)
# Whoops, this doesn't look so good! Let's try to do this more "manually" instead.
# We'll use NaturalNeighbours.jl for this, but we first have to reconstruct the triangulation
# with the same faces, but in a Bertin projection...
using NaturalNeighbours
lonlat2bertin = Proj.Transformation(GFT.EPSG(4326), GFT.ProjString("+proj=bertin1953 +type=crs +lon_0=-16.5 +lat_0=-42 +x_0=7.93 +y_0=0.09"); always_xy = true)
lons = LinRange(-180, 180, 300)
lats = LinRange(-90, 90, 150)
bertin_points = lonlat2bertin.(lons, lats')

projected_points = GO.reproject(GO.tuples(points), source_crs = GFT.EPSG(4326), target_crs = "+proj=bertin1953 +lon_0=-16.5 +lat_0=-42 +x_0=7.93 +y_0=0.09")

ch = DelTri.convex_hull(projected_points) # assumes each point is in the triangulation
Copy link
Contributor

@DanielVandH DanielVandH Jul 21, 2024

Choose a reason for hiding this comment

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

@asinghvi17 projected_points is three-dimensional. The convex_hull function is treating it by ignoring the z coordinate of each point. What is supposed to be the input here? This is the issue you're having with NaturalNeighbours - the triangulation is very invalid.

image

julia> DelTri.validate_triangulation(tri) # on main
The adjacent2vertex map is inconsistent with the adjacent map. The vertex 1953 and edge (2026, 2008) are inconsistent with the associated edge set Set([(2026, 2008), (1951, 2026), (2008, 1952), (1952, 1951)]) from the adjacent2vertex map, as (2026, 2008) maps to -1 instead of 1953.
The vertex 2365, which defines the edge (-1, 2365) with adjacent vertex 2365, is inconsistent with the graph which fails to include at least one of 2037 and -1 in 2365's neighbourhood.
The triangle (2379, 2381, 2382) is incorrectly stored in the adjacent map, with the edge (2382, 2379) instead mapping to -1.
The Delaunay criterion does not hold for the triangle-vertex pair ((1879, 763, 1464), 1444).
The graph is inconsistent with the triangle set. The triangle (1732, 2039, -1) is in the triangle set but either 2039 or -1 are not in 1732's neighbourhood.
The triangle (757, 691, 136) is negatively oriented.
The edge iterator has duplicate edges.
The solid_edge iterator has duplicate edges.

Copy link
Member Author

Choose a reason for hiding this comment

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

projected_points' z coordinate only carries metadata, so it's fine for it to be three-dimensional. That being said I haven't checked the triangulation in stereographic space, it might have issues there too.

Copy link
Member Author

Choose a reason for hiding this comment

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

On main the boundary points generate degenerate triangles, I guess I can remove those?

Copy link
Contributor

@DanielVandH DanielVandH Jul 22, 2024

Choose a reason for hiding this comment

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

I'm surprised that it's generating triangles where some of the vertices repeat..

The issue to me just looks like the triangles are somehow calculated incorrectly. In the plot I show at least the triangulation is non-planar

Copy link
Member Author

Choose a reason for hiding this comment

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

That's not entirely unexpected, since the triangulation is computed in stereographic space (where the triangulation generated is planar, correct, etc) and not in the projected space that we are currently in.

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, that would be a problem. Maybe it's best to create some kind of spherical triangulation type, which can also handle spherical Voronoi? That sounds like a lot of effort though.

Copy link
Contributor

@DanielVandH DanielVandH Jul 26, 2024

Choose a reason for hiding this comment

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

I don't really know how you would get around this issue. It just seems like a bug somewhere to me.. the projection isn't an isometry but it should be conformal, right? I think conformal maps would show up better than what we see in the image above, e.g. it should preserve angles. The crossings seem to come only from boundary vertices (from a visual check anyway, who knows about the interior) which suggests you're missing something when handling only the interior triangles.

If everything is fine and there are in fact no bugs in the actual creation and my thinking is incorrect, it would be interesting to know how the other library handles this. Maybe there are things I need to do on DelaunayTriangulation's side to help

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, to clarify - the triangulation is done in stereographic space, where the triangulation would be completely valid. I'm trying to interpolate in the Bertin 1953 projection, which doesn't have the same conformal property as the stereographic projection. I guess what I was trying to do is actually the incorrect thing, and I should be figuring out how to interpolate on the sphere instead...

Copy link
Contributor

Choose a reason for hiding this comment

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

I see.. my thinking was that you can do everything on the valid triangulation and then lift it back up to the sphere. I'll probably have to look more at the algorithm itself to understand.

Copy link
Member Author

Choose a reason for hiding this comment

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

At least for interpolation, I would expect that to have issues when getting the values of points near the chosen pivot point in spherical space - there's always a point which wouldn't be connected to the triangulation in stereographic space, and neighbours in spherical space would be separated by almost the entire domain of points.

The first problem could be solved by injecting a rectangular boundary, but I'm not really sure how to deal with the second problem.

boundary_nodes = DelTri.get_vertices(ch)
bertin_boundary_poly = GI.Polygon([GI.LineString(DelTri.get_points(ch)[DelTri.get_vertices(ch)])])
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you need bertin_boundary_poly for? Are you just using it to see if a point is inside the triangulation? You could look at DelaunayTriangulation.dist(tri, q) I think

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 was trying to see if I could filter out points that are outside the convex hull and therefore not within any triangle of tri, but no luck with that.


tri = DelTri.Triangulation(projected_points, faces, boundary_nodes)
itp = NaturalNeighbours.interpolate(tri, last.(points); derivatives = true)

mat = [
if GO.contains(bertin_boundary_poly, (x, y))
itp(x, y; method = Nearest())
else
NaN
end
for (x, y) in bertin_points
]
# TODO: this currently doesn't work, because some points are not inside a triangle and so cannot be located.
# Options are:
# 1. Reject all points outside the convex hull of the projected points. (Tried but failed)
# 2. ...
Copy link
Contributor

Choose a reason for hiding this comment

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

What goes wrong here? Can you just do something like

xv, yv = first.(bertin_points), last.(bertin_points)
vals = itp(xv, yv, method = Nearest(), project = false) # Nearest() doesn't seem to use project...
vals[identify_exterior_points(xv, yv, itp)] .= NaN 

Copy link
Member Author

Choose a reason for hiding this comment

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

ERROR: TaskFailedException

    nested task error: The point, (-9.986746533293853e6, -6.557879586899938e6), could not be located.
    Stacktrace:
      [1] brute_force_search(tri::DelaunayTriangulation.Triangulation{…}, q::Tuple{…}; itr::Set{…})
        @ DelaunayTriangulation ~/.julia/dev/DelaunayTriangulation/src/algorithms/point_location/brute_force.jl:36
      [2] brute_force_search
        @ ~/.julia/dev/DelaunayTriangulation/src/algorithms/point_location/brute_force.jl:31 [inlined]
      [3] restart_jump_and_march(tri::DelaunayTriangulation.Triangulation{…}, q::Tuple{…}, store_history::Val{…}, history::Nothing, rng::Random.TaskLocalRNG, maxiters::Int64, cur_iter::Int64, concavity_protection::Bool, num_restarts::Int64, use_barriers::Val{…})
        @ DelaunayTriangulation ~/.julia/dev/DelaunayTriangulation/src/algorithms/point_location/jump_and_march.jl:1236
      [4] _jump_and_march(tri::DelaunayTriangulation.Triangulation{…}, q::Tuple{…}, k::Int64, store_history::Val{…}, history::Nothing, rng::Random.TaskLocalRNG, maxiters::Int64, cur_iter::Int64, concavity_protection::Bool, num_restarts::Int64, use_barriers::Val{…})
        @ DelaunayTriangulation ~/.julia/dev/DelaunayTriangulation/src/algorithms/point_location/jump_and_march.jl:1145
      [5] restart_jump_and_march(tri::DelaunayTriangulation.Triangulation{…}, q::Tuple{…}, store_history::Val{…}, history::Nothing, rng::Random.TaskLocalRNG, maxiters::Int64, cur_iter::Int64, concavity_protection::Bool, num_restarts::Int64, use_barriers::Val{…})
        @ DelaunayTriangulation ~/.julia/dev/DelaunayTriangulation/src/algorithms/point_location/jump_and_march.jl:1234
    --- the last 2 lines are repeated 23 more times ---
     [52] _jump_and_march(tri::DelaunayTriangulation.Triangulation{…}, q::Tuple{…}, k::Int64, store_history::Val{…}, history::Nothing, rng::Random.TaskLocalRNG, maxiters::Int64, cur_iter::Int64, concavity_protection::Bool, num_restarts::Int64, use_barriers::Val{…})
        @ DelaunayTriangulation ~/.julia/dev/DelaunayTriangulation/src/algorithms/point_location/jump_and_march.jl:1145
     [53] #jump_and_march#87
        @ ~/.julia/dev/DelaunayTriangulation/src/algorithms/point_location/jump_and_march.jl:739 [inlined]
     [54] jump_to_voronoi_polygon(tri::DelaunayTriangulation.Triangulation{…}, q::Tuple{…}; kwargs::@Kwargs{…})
        @ DelaunayTriangulation ~/.julia/dev/DelaunayTriangulation/src/algorithms/point_location/nearest_neighbour.jl:24
     [55] jump_to_voronoi_polygon
        @ ~/.julia/dev/DelaunayTriangulation/src/algorithms/point_location/nearest_neighbour.jl:23 [inlined]
     [56] _compute_nearest_coordinates(tri::DelaunayTriangulation.Triangulation{…}, interpolation_point::Tuple{…}, cache::NaturalNeighbours.NaturalNeighboursCache{…}; kwargs::@Kwargs{…})
        @ NaturalNeighbours ~/.julia/packages/NaturalNeighbours/HJ2P0/src/interpolation/coordinates/nearest.jl:10
     [57] _compute_nearest_coordinates
        @ ~/.julia/packages/NaturalNeighbours/HJ2P0/src/interpolation/coordinates/nearest.jl:1 [inlined]
     [58] #compute_natural_coordinates#28
        @ ~/.julia/packages/NaturalNeighbours/HJ2P0/src/interpolation/coordinates/nearest.jl:19 [inlined]
     [59] compute_natural_coordinates
        @ ~/.julia/packages/NaturalNeighbours/HJ2P0/src/interpolation/coordinates/nearest.jl:18 [inlined]
     [60] #_eval_interp#21
        @ ~/.julia/packages/NaturalNeighbours/HJ2P0/src/interpolation/eval.jl:42 [inlined]
     [61] _eval_interp
        @ ~/.julia/packages/NaturalNeighbours/HJ2P0/src/interpolation/eval.jl:40 [inlined]
     [62] (::NaturalNeighbours.NaturalNeighboursInterpolant{…})(x::Float64, y::Float64, id::Int64; parallel::Bool, method::Nearest{…}, kwargs::@Kwargs{…})
        @ NaturalNeighbours ~/.julia/packages/NaturalNeighbours/HJ2P0/src/interpolation/interpolate.jl:187
     [63] NaturalNeighboursInterpolant
        @ ~/.julia/packages/NaturalNeighbours/HJ2P0/src/interpolation/interpolate.jl:180 [inlined]
     [64] macro expansion
        @ ~/.julia/packages/NaturalNeighbours/HJ2P0/src/interpolation/interpolate.jl:204 [inlined]
     [65] (::NaturalNeighbours.var"#71#threadsfor_fun#18"{…})(tid::Int64; onethread::Bool)
        @ NaturalNeighbours ./threadingconstructs.jl:215
     [66] #71#threadsfor_fun
        @ ./threadingconstructs.jl:182 [inlined]

...and 7 more exceptions.

Stacktrace:
 [1] threading_run(fun::NaturalNeighbours.var"#71#threadsfor_fun#18"{…}, static::Bool)
   @ Base.Threads ./threadingconstructs.jl:172
 [2] macro expansion
   @ ./threadingconstructs.jl:220 [inlined]
 [3] (::NaturalNeighbours.NaturalNeighboursInterpolant{…})(vals::Vector{…}, x::Vector{…}, y::Vector{…}; parallel::Bool, method::Nearest{…}, kwargs::@Kwargs{…})
   @ NaturalNeighbours ~/.julia/packages/NaturalNeighbours/HJ2P0/src/interpolation/interpolate.jl:202

Copy link
Contributor

Choose a reason for hiding this comment

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

That's... concerning. That should be impossible since it should be finding it in a ghost triangle at the very least... will take a look and actually run the code later.

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 one problem is that the mesh and convex hull here don't actually cover the full Earth - see mesh(bertin_points, faces)
download-12

Copy link
Member Author

Choose a reason for hiding this comment

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






# Now, we proceed with the script implementation which has quite a bit of debug information + plots
pivot_ind = findfirst(isfinite, points)

# debug
point_colors = fill(:blue, length(points))
point_colors[pivot_ind] = :red
# end debug

pivot_point = points[pivot_ind]
necessary_rotation = #=Rotations.RotY(-π) *=# Rotations.RotY(-deg2rad(90-pivot_point[2])) * Rotations.RotZ(-deg2rad(pivot_point[1]))
#
net_transformation_to_corrected_cartesian = CoordinateTransformations.LinearMap(necessary_rotation) ∘ UnitCartesianFromGeographic()

scatter(map(net_transformation_to_corrected_cartesian, points); color = point_colors)
#
stereographic_points = (StereographicFromCartesian() ∘ net_transformation_to_corrected_cartesian).(points)
scatter(stereographic_points; color = point_colors)
#
triangulation_points = copy(stereographic_points)


# Iterate through the points and find infinite/invalid points
max_radius = 1
really_far_idxs = Int[]
for (i, point) in enumerate(stereographic_points)
m = hypot(point[1], point[2])
if !isfinite(m) || m > 1e32
push!(really_far_idxs, i)
elseif m > max_radius
max_radius = m
end
end
@show max_radius length(really_far_idxs)
far_value = 1e6 * sqrt(max_radius)

if !isempty(really_far_idxs)
triangulation_points[really_far_idxs] .= (Point2(far_value, 0.0),)
end

boundary_points = reverse([
(-far_value, -far_value),
(-far_value, far_value),
(far_value, far_value),
(far_value, -far_value),
(-far_value, -far_value),
])

# boundary_nodes, pts = DelTri.convert_boundary_points_to_indices(boundary_points; existing_points=triangulation_points)

# triangulation = DelTri.triangulate(triangulation_points; boundary_nodes)
triangulation = DelTri.triangulate(triangulation_points)
triplot(triangulation)
# for diagnostics, try `fig, ax, sc = triplot(triangulation, show_constrained_edges=true, show_convex_hull=true)`

# First, get all the "solid" faces, ie, faces not attached to boundary nodes
boundary_faces = Iterators.filter(Base.Fix1(DelTri.is_boundary_triangle, triangulation), DelTri.each_solid_triangle(triangulation)) |> collect
faces = map(CairoMakie.GeometryBasics.TriangleFace, DelTri.each_solid_triangle(triangulation))

for ghost_face in DelTri.each_ghost_triangle(triangulation)
push!(faces, CairoMakie.GeometryBasics.TriangleFace(map(ghost_face) do i; i ≤ 0 ? pivot_ind : i end))
end

wireframe(CairoMakie.GeometryBasics.normal_mesh(map(UnitCartesianFromGeographic(), points), faces))

grid_lons = LinRange(-180, 180, 10)
grid_lats = LinRange(-90, 90, 10)
lon_grid = [CairoMakie.GeometryBasics.LineString([Point{2, Float64}((grid_lons[j], -90)), Point{2, Float64}((grid_lons[j], 90))]) for j in 1:10] |> vec
lat_grid = [CairoMakie.GeometryBasics.LineString([Point{2, Float64}((-180, grid_lats[i])), Point{2, Float64}((180, grid_lats[i]))]) for i in 1:10] |> vec

sampled_grid = GO.segmentize(lat_grid; max_distance = 0.01)

geographic_grid = GO.transform(UnitCartesianFromGeographic(), sampled_grid) .|> x -> GI.LineString{true, false}(x.geom)
stereographic_grid = GO.transform(sampled_grid) do point
(StereographicFromCartesian() ∘ UnitCartesianFromGeographic())(point)
end .|> x -> GI.LineString{false, false}(x.geom)



fig = Figure(); ax = LScene(fig[1, 1])
for (i, line) in enumerate(geographic_grid)
lines!(ax, line; linewidth = 3, color = Makie.resample_cmap(:viridis, length(stereographic_grid))[i])
end
fig

#
fig = Figure(); ax = LScene(fig[1, 1])
for (i, line) in enumerate(stereographic_grid)
lines!(ax, line; linewidth = 3, color = Makie.resample_cmap(:viridis, length(stereographic_grid))[i])
end
fig

all(reduce(vcat, faces) |> sort! |> unique! .== 1:length(points))

faces[findall(x -> 1 in x, faces)]

# try NaturalNeighbours, fail miserably
Copy link
Contributor

Choose a reason for hiding this comment

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

Are there issues even when using Sibson(1) below? Or what's the misery

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 I'm not quite sure what the issue is here, in the worst case I can always sample in stereographic space but that seems pretty bad...

using NaturalNeighbours

itp = NaturalNeighbours.interpolate(triangulation, last.(points); derivatives = true)
lons = LinRange(-180, 180, 360)
lats = LinRange(-90, 90, 180)
_x = vec([x for x in lons, _ in lats])
_y = vec([y for _ in lons, y in lats])

sibson_1_vals = itp(_x, _y; method=Sibson(1))

# necessary coordinate transformations

struct StereographicFromCartesian <: CoordinateTransformations.Transformation
end

function (::StereographicFromCartesian)(xyz::AbstractVector)
@assert length(xyz) == 3 "StereographicFromCartesian expects a 3D Cartesian vector"
x, y, z = xyz
# The Wikipedia definition has the north pole at infinity,
# this implementation has the south pole at infinity.
return Point2(x/(1-z), y/(1-z))
end

struct CartesianFromStereographic <: CoordinateTransformations.Transformation
end

function (::CartesianFromStereographic)(stereographic_point)
X, Y = stereographic_point
x2y2_1 = X^2 + Y^2 + 1
return Point3(2X/x2y2_1, 2Y/x2y2_1, (x2y2_1 - 2)/x2y2_1)
end

struct UnitCartesianFromGeographic <: CoordinateTransformations.Transformation
end

function (::UnitCartesianFromGeographic)(geographic_point)
# Longitude is directly translatable to a spherical coordinate
# θ (azimuth)
θ = deg2rad(GI.x(geographic_point))
# The polar angle is 90 degrees minus the latitude
# ϕ (polar angle)
ϕ = deg2rad(90 - GI.y(geographic_point))
# Since this is the unit sphere, the radius is assumed to be 1,
# and we don't need to multiply by it.
return Point3(
sin(ϕ) * cos(θ),
sin(ϕ) * sin(θ),
cos(ϕ)
)
end

struct GeographicFromUnitCartesian <: CoordinateTransformations.Transformation
end

function (::GeographicFromUnitCartesian)(xyz::AbstractVector)
@assert length(xyz) == 3 "GeographicFromUnitCartesian expects a 3D Cartesian vector"
x, y, z = xyz
return Point2(
atan(y, x),
atan(hypot(x, y), z),
)
end

transformed_points = GO.transform(points) do point
# first, spherical to Cartesian
longitude, latitude = deg2rad(GI.x(point)), deg2rad(GI.y(point))
radius = 1 # we will operate on the unit sphere
xyz = Point3d(
radius * sin(latitude) * cos(longitude),
radius * sin(latitude) * sin(longitude),
radius * cos(latitude)
)
# then, rotate Cartesian so the pivot point is at the south pole
rotated_xyz = Rotations.AngleAxis
end




function randsphericalangles(n)
θ = 2π .* rand(n)
ϕ = acos.(2 .* rand(n) .- 1)
return Point2.(θ, ϕ)
end

function randsphere(n)
θϕ = randsphericalangles(n)
return Point3.(
sin.(last.(θϕs)) .* cos.(first.(θϕs)),
sin.(last.(θϕs)) .* sin.(first.(θϕs)),
cos.(last.(θϕs))
)
end

θϕs = randsphericalangles(50)
θs, ϕs = first.(θϕs), last.(θϕs)
pts = Point3.(
sin.(ϕs) .* cos.(θs),
sin.(ϕs) .* sin.(θs),
cos.(ϕs)
)

f, a, p = scatter(pts; color = [fill(:blue, 49); :red])

function Makie.rotate!(t::Makie.Transformable, rotation::Rotations.Rotation)
quat = Rotations.QuatRotation(rotation)
Makie.rotate!(Makie.Absolute, t, Makie.Quaternion(quat.q.v1, quat.q.v2, quat.q.v3, quat.q.s))
end


rotate!(p, Rotations.RotX(π/2))
rotate!(p, Rotations.RotX(0))

pivot_point = θϕs[end]
necessary_rotation = Rotations.RotY(pi) * Rotations.RotY(-pivot_point[2]) * Rotations.RotZ(-pivot_point[1])

rotate!(p, necessary_rotation)

f
Loading