Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 12 additions & 6 deletions src/e3sm_quickview/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ def _build_ui(self, **_):
ProjectionEquidistant="projection = ['Cyl. Equidistant']",
ProjectionRobinson="projection = ['Robinson']",
ProjectionMollweide="projection = ['Mollweide']",
ProjectionSpherical="projection = ['Spherical']",
FileOpen=(self.toggle_toolbar, "['load-data']"),
SaveState="trigger('download_state_dialog')",
UploadState="utils.get('document').querySelector('#fileUpload').click()",
Expand All @@ -196,6 +197,7 @@ def _build_ui(self, **_):
mt.bind("c", "ProjectionEquidistant")
mt.bind("r", "ProjectionRobinson")
mt.bind("m", "ProjectionMollweide")
mt.bind("n", "ProjectionSpherical")

mt.bind("f", "FileOpen")
mt.bind("e", "SaveState")
Expand Down Expand Up @@ -648,12 +650,16 @@ async def _on_projection(self, projection, **_):
self.source.UpdatePipeline()
self.view_manager.reset_camera()

# Hack to force reset_camera for "cyl mode"
# => may not be needed if we switch to rca
if " " in proj_str:
for _ in range(2):
await asyncio.sleep(0.1)
self.view_manager.reset_camera()
@change("spherical_center_lat", "spherical_center_lon", "projection")
def _on_center(self, spherical_center_lat, spherical_center_lon, projection, **_):
if projection == ["Spherical"]:
self.source.Clip(self.view_manager._clip_plane)
self.view_manager.center_camera(
float(spherical_center_lat), float(spherical_center_lon)
)
else:
self.source.Clip()
self.view_manager.camera_projection()

def _on_slicing_change(self, var, ind_var, **_):
with perf.timed(f"tick.{var}={self.state[ind_var]}.total"):
Expand Down
4 changes: 2 additions & 2 deletions src/e3sm_quickview/components/doc.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from trame.widgets import html
from trame.widgets import vuetify3 as v3

from e3sm_quickview.assets import ASSETS

# -----------------------------------------------------------------------------
# Tools
Expand Down Expand Up @@ -72,12 +71,13 @@ def __init__(self):
super().__init__(
icon="mdi-earth",
title="Map Projection",
description="Select projection to use for the visualizations: cylindrical equidistant, Robinson, Mollweide.",
description="Select projection to use for the visualizations: cylindrical equidistant, Robinson, Mollweide, Spherical.",
)
with self, v3.Template(v_slot_append=True):
v3.VHotkey(keys="c", variant="contained", inline=True)
v3.VHotkey(keys="r", variant="contained", inline=True)
v3.VHotkey(keys="m", variant="contained", inline=True)
v3.VHotkey(keys="n", variant="contained", inline=True)


class ToolLayoutManagement(Tool):
Expand Down
49 changes: 49 additions & 0 deletions src/e3sm_quickview/components/toolbars.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,60 @@ def __init__(
self.state.setdefault("show_zoom_controls", False)
self.state.setdefault("show_pan_controls", False)
self.state.setdefault("show_aspect_ratio", False)
self.state.setdefault("show_spherical_center", False)

with self:
v3.VIcon("mdi-view-module", classes="px-6 opacity-50")
v3.VSpacer()
with html.Div(classes="d-flex ga-2 align-center"):
# --- Spherical projection center ---
with v3.VSheet(
classes="d-flex align-center rounded px-1 ga-1 py-1",
color=("show_spherical_center ? 'grey-lighten-3' : 'transparent'",),
v_if="projection[0] === 'Spherical'",
):
v3.VIconBtn(
v_tooltip_bottom="'Toggle Spherical center'",
icon="mdi-target",
flat=True,
click="show_spherical_center = !show_spherical_center",
color=("show_spherical_center ? 'primary' : ''",),
size=("show_spherical_center ? 'small' : 'default'",),
classes=("show_spherical_center ? 'ml-1' : 'rounded'",),
)
with (
v3.VExpandXTransition(),
html.Div(
v_if="show_spherical_center",
classes="d-flex align-center ga-1",
),
):
v3.VDivider(vertical=True, classes="mx-1")
v3.VNumberInput(
label="Latitude",
v_model=("spherical_center_lat", 0),
precision=1,
min=("-90",),
max=("90",),
step=("0.5",),
hide_details=True,
density="compact",
variant="plain",
style="min-width: 10rem;",
)
v3.VNumberInput(
label="Longitude",
v_model=("spherical_center_lon", 0),
precision=1,
min=("-180",),
max=("180",),
step=("0.5",),
hide_details=True,
density="compact",
variant="plain",
style="min-width: 10rem;",
)

# --- Aspect ratio toggle + slider ---
with v3.VSheet(
classes="d-flex align-center rounded px-1 ga-1 py-1",
Expand Down
5 changes: 5 additions & 0 deletions src/e3sm_quickview/components/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@ def options(self):
"value": "Mollweide",
"key": "m",
},
{
"title": "Spherical",
"value": "Spherical",
"key": "n",
},
]


Expand Down
18 changes: 18 additions & 0 deletions src/e3sm_quickview/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ def projection(self, value):
self._projection = value
self.proj.Projection = value

def update(self):
self.geometry.UpdatePipeline()


class GridLines:
def __init__(self, projection="Mollweide"):
Expand Down Expand Up @@ -123,6 +126,10 @@ def projection(self, value):
self._projection = value
self.proj.Projection = value

def update(self):
self.geometry.UpdatePipeline()
self.mapper.Update()


class DataReader:
def __init__(self, projection="Mollweide"):
Expand Down Expand Up @@ -286,6 +293,9 @@ def UpdateProjection(self, proj):
self.data_reader.projection = proj
self.grid_lines.projection = proj
self.continent.projection = proj
self.data_reader.update()
self.grid_lines.update()
self.continent.update()

def UpdatePipeline(self, time=0.0):
self.data_reader.update(time)
Expand All @@ -308,6 +318,14 @@ def LoadVariables(self, vars):

self.data_reader.reader.Variables = list(set([*vars, "lat", "lon"]))

def Clip(self, plane=None):
self.grid_lines.mapper.RemoveAllClippingPlanes()
self.continent.mapper.RemoveAllClippingPlanes()

if plane:
self.grid_lines.mapper.AddClippingPlane(plane)
self.continent.mapper.AddClippingPlane(plane)


if __name__ == "__main__":
e = EAMVisSource()
61 changes: 41 additions & 20 deletions src/e3sm_quickview/plugins/eam_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from vtkmodules.util import numpy_support, vtkConstants
from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase


# Number of threads for the projection fan-out. pyproj releases the GIL
# inside Transformer.transform, so chunking the input across threads
# scales nearly linearly (7.4x on 8 threads in our bench). Default is
Expand Down Expand Up @@ -75,6 +76,7 @@ def work(i):
y_out = np.concatenate([r[1] for r in results])
return x_out, y_out


try:
import warnings

Expand Down Expand Up @@ -204,7 +206,7 @@ def add_cell_arrays(inData, outData, cached_output):
out_np = numpy_support.vtk_to_numpy(out_array)
for s, e in zip(starts, ends):
src_off = int(pid_np[s])
out_np[s:e] = in_np[src_off:src_off + (e - s)]
out_np[s:e] = in_np[src_off : src_off + (e - s)]
out_array.Modified()


Expand Down Expand Up @@ -381,6 +383,7 @@ def RequestData(self, request, inInfo, outInfo):
<Entry value="0" text="Cyl. Equidistant"/>
<Entry value="1" text="Robinson"/>
<Entry value="2" text="Mollweide"/>
<Entry value="3" text="Spherical"/>
</EnumerationDomain>
</IntVectorProperty>
"""
Expand Down Expand Up @@ -446,34 +449,52 @@ def RequestData(self, request, inInfo, outInfo):
x = flat[0::3] - 180.0 if self.translate else flat[0::3]
y = flat[1::3]

try:
# Use proj4 string for WGS84 instead of EPSG code to avoid database dependency
latlon = Proj(proj="latlong", datum="WGS84")
if self.project == 1:
proj = Proj(proj="robin")
elif self.project == 2:
proj = Proj(proj="moll")
else:
# Should not reach here, but return without transformation
if self.project == 3:
# Spherical
to_rad = np.pi / 180
x_rad = x * to_rad
y_rad = y * to_rad
cos_y_rad = np.cos(y_rad)
xs = np.cos(x_rad) * cos_y_rad
zs = np.sin(x_rad) * cos_y_rad
ys = np.sin(y_rad)
flat[0::3] = xs
flat[1::3] = ys
flat[2::3] = zs
else:
try:
# Use proj4 string for WGS84 instead of EPSG code to avoid database dependency
latlon = Proj(proj="latlong", datum="WGS84")
if self.project == 1:
proj = Proj(proj="robin")
elif self.project == 2:
proj = Proj(proj="moll")
else:
# Should not reach here, but return without transformation
return 1

xformer = Transformer.from_proj(
latlon, proj, always_xy=True
)
with _perf.timed("project.pyproj_transform"):
res = _threaded_transform(xformer, x, y)
except Exception as e:
print(f"Projection error: {e}")
# If projection fails, return without modifying coordinates
return 1

xformer = Transformer.from_proj(latlon, proj, always_xy=True)
with _perf.timed("project.pyproj_transform"):
res = _threaded_transform(xformer, x, y)
except Exception as e:
print(f"Projection error: {e}")
# If projection fails, return without modifying coordinates
return 1
flat[0::3] = np.array(res[0])
flat[1::3] = np.array(res[1])
flat[0::3] = np.array(res[0])
flat[1::3] = np.array(res[1])

outPoints = flat.reshape(out_points_np.shape)
_coords = numpy_support.numpy_to_vtk(outPoints, deep=True)
outData.GetPoints().SetData(_coords)
# the previous cached_points, if any, is available for
# garbage collection after this assignment
self.cached_points = out_points_vtk
self._cached_input_points = in_points # hold ref so id() stays valid
self._cached_input_points = (
in_points # hold ref so id() stays valid
)
self._cached_key = cache_key

return 1
Expand Down
24 changes: 24 additions & 0 deletions src/e3sm_quickview/view_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from trame.ui.html import DivLayout
from trame.widgets import client, colormaps, rca
from trame.widgets import vuetify3 as v3
from vtkmodules.vtkCommonDataModel import vtkPlane
from vtkmodules.vtkRenderingCore import (
vtkCamera,
vtkCellPicker,
Expand Down Expand Up @@ -56,6 +57,7 @@ def __init__(self, server, source):
super().__init__(server)
self.use_image_stream = True
self._camera = vtkCamera(parallel_projection=1)
self._clip_plane = vtkPlane()
self._render_window = vtkRenderWindow()
self._render_window.OffScreenRenderingOn()
self._picker = vtkCellPicker(tolerance=0.0005)
Expand Down Expand Up @@ -160,6 +162,28 @@ def refresh_ui(self, **_):
for view in self._var2view.values():
view._build_ui()

def camera_projection(self):
self._camera.focal_point = (0, 0, 0)
self._camera.position = (0, 0, 1)
self._camera.view_up = (0, 1, 0)
self.reset_camera()

def center_camera(self, lat, lon):
x = math.cos(lon * math.pi / 180) * math.cos(lat * math.pi / 180)
z = math.sin(lon * math.pi / 180) * math.cos(lat * math.pi / 180)
y = math.sin(lat * math.pi / 180)
self._camera.focal_point = (0, 0, 0)
self._camera.position = (x, y, z)
self._camera.view_up = (
-math.cos(lon * math.pi / 180),
2,
-math.sin(lon * math.pi / 180),
)

self._clip_plane.origin = (0, 0, 0)
self._clip_plane.normal = (x, y, z)
self.reset_camera()

def reset_camera(self, render=True):
if self.layout_dirty or not self._last_vars:
self.pending_reset_camera = 1
Expand Down
1 change: 1 addition & 0 deletions src/e3sm_quickview/view_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ def bounds(self, v):

def reset_camera(self):
self.renderer.ResetCameraScreenSpace(0.9)
self.renderer.ResetCameraClippingRange()

def update_size(self, size):
new_size = (int(size["w"] * size["p"]), int(size["h"] * size["p"]))
Expand Down
Loading