diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd7535d..51ed6ec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,7 @@ repos: rev: "v4.6.0" hooks: - id: "check-added-large-files" + args: ["--maxkb=20000"] - id: "check-ast" - id: "check-byte-order-marker" - id: "check-docstring-first" diff --git a/notebooks/TELEMAC.ipynb b/notebooks/TELEMAC.ipynb new file mode 100644 index 0000000..d93cba8 --- /dev/null +++ b/notebooks/TELEMAC.ipynb @@ -0,0 +1,134 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ensure that the Selafin engine is available\n", + "%pip install xarray-selafin" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# main imports\n", + "import holoviews as hv\n", + "hv.extension(\"bokeh\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import thalassa\n", + "from thalassa import api" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "malpasset = api.open_dataset('../tests/data/r2d_malpasset-char_p2.slf', source_crs = None) # default EPSG is 4326\n", + "malpasset" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `Malpasset` mesh is not georeferentiated in a known local coordinate system\n", + "\n", + "We'll avoid activating the default EPSG (4326) in order to be able to view it. \n", + "\n", + "by imposing `source_crs = None`, we disable: \n", + " * reprojection done on the mesh (Thalassa reprojects automatically to Mercator to superpose meshes with WMS tiles) \n", + " * the tiling visualization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# The trimesh is the most basic object. This is what you need to create all the others graphs\n", + "# It is on this object that you specify the timestamp and/or the layer.\n", + "trimesh = api.create_trimesh(malpasset.isel(time=0), variable='H')\n", + "\n", + "# The nodes of the mesh (without triangles, just the points!)\n", + "nodes = api.get_nodes(trimesh)\n", + "\n", + "# The wireframe is the representation of the mesh\n", + "wireframe = api.get_wireframe(trimesh)\n", + "\n", + "# The raster object is the basic Map that visualizes the variable. \n", + "# You can specify things like the colorbar limits and/or the extents\n", + "#raster = api.get_raster(trimesh, clim_min=0, clim_max=15)\n", + "raster = api.get_raster(trimesh).opts(cmap = 'rainbow')\n", + "\n", + "(raster * wireframe).opts(width=900, height=600)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To show another example using a known CRS, here is the `Manche` mesh from the TOMAWAC examples:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "manche = api.open_dataset('../tests/data/r2d.V1P3.slf') # use the default EPSG (4326)\n", + "manche" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "thalassa.plot(\n", + " manche.isel(time = -1), \n", + " variable='HAUTEUR_HM0', \n", + " show_mesh = True, \n", + " cmap = 'rainbow'\n", + ").opts(width=900, height = 900)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/poetry.lock b/poetry.lock index 8519ca6..19c0d03 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "appnope" @@ -1040,13 +1040,13 @@ test = ["fiona[s3]", "pytest (>=7)", "pytest-cov", "pytz"] [[package]] name = "flox" -version = "0.9.7" +version = "0.9.8" description = "GroupBy operations for dask.array" optional = false python-versions = ">=3.9" files = [ - {file = "flox-0.9.7-py3-none-any.whl", hash = "sha256:0e15d678c5f3d46fe5c6481519d01ceae40a111133b110e80f3b274881af8497"}, - {file = "flox-0.9.7.tar.gz", hash = "sha256:baa7c0aa9b2836f5cf1b283ce918cf3d61dc9ff0af8bda026a598ba5cc0b7c68"}, + {file = "flox-0.9.8-py3-none-any.whl", hash = "sha256:9a74daff7e47d105c98fa506f25c55b52c38374e96f046ca906b49272baec03f"}, + {file = "flox-0.9.8.tar.gz", hash = "sha256:1236efb869ae3b9dec2066605dbacebae571721ab7d37507cbb0d96bbc3e47fd"}, ] [package.dependencies] @@ -1475,6 +1475,27 @@ qtconsole = ["qtconsole"] test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] +[[package]] +name = "ipywidgets" +version = "8.1.3" +description = "Jupyter interactive widgets" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ipywidgets-8.1.3-py3-none-any.whl", hash = "sha256:efafd18f7a142248f7cb0ba890a68b96abd4d6e88ddbda483c9130d12667eaf2"}, + {file = "ipywidgets-8.1.3.tar.gz", hash = "sha256:f5f9eeaae082b1823ce9eac2575272952f40d748893972956dc09700a6392d9c"}, +] + +[package.dependencies] +comm = ">=0.1.3" +ipython = ">=6.1.0" +jupyterlab-widgets = ">=3.0.11,<3.1.0" +traitlets = ">=4.3.1" +widgetsnbextension = ">=4.0.11,<4.1.0" + +[package.extras] +test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"] + [[package]] name = "jedi" version = "0.19.1" @@ -1546,6 +1567,26 @@ files = [ [package.dependencies] referencing = ">=0.31.0" +[[package]] +name = "jupyter-bokeh" +version = "4.0.4" +description = "A Jupyter extension for rendering Bokeh content." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_bokeh-4.0.4-py3-none-any.whl", hash = "sha256:5aba6ebf1b0db56b613c1d197fb54b8c3260363864bd6a24b2c01024e8c5b328"}, + {file = "jupyter_bokeh-4.0.4.tar.gz", hash = "sha256:f70b70b55c2e53b7d34bf6a244d489dd6b7824d0318790181463f481cc2cd1f8"}, +] + +[package.dependencies] +bokeh = "==3.*" +ipywidgets = "==8.*" + +[package.extras] +all = ["jupyter-bokeh[build]", "jupyter-bokeh[tests]"] +build = ["jupyterlab (>=4.0,<5.0)", "setuptools (>=40.8.0)"] +tests = ["flake8", "pytest"] + [[package]] name = "jupyter-client" version = "8.6.2" @@ -1589,6 +1630,17 @@ traitlets = ">=5.3" docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"] +[[package]] +name = "jupyterlab-widgets" +version = "3.0.11" +description = "Jupyter interactive widgets for JupyterLab" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jupyterlab_widgets-3.0.11-py3-none-any.whl", hash = "sha256:78287fd86d20744ace330a61625024cf5521e1c012a352ddc0a3cdc2348becd0"}, + {file = "jupyterlab_widgets-3.0.11.tar.gz", hash = "sha256:dd5ac679593c969af29c9bed054c24f26842baa51352114736756bc035deee27"}, +] + [[package]] name = "kiwisolver" version = "1.4.5" @@ -2242,7 +2294,6 @@ files = [ {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, - {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] [[package]] @@ -2658,7 +2709,6 @@ files = [ {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, - {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, @@ -3665,13 +3715,13 @@ files = [ [[package]] name = "requests" -version = "2.32.2" +version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ - {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"}, - {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -4187,6 +4237,17 @@ files = [ docs = ["Sphinx (>=1.7.5)", "pylons-sphinx-themes"] testing = ["coverage", "pytest (>=3.1.0)", "pytest-cov", "pytest-xdist"] +[[package]] +name = "widgetsnbextension" +version = "4.0.11" +description = "Jupyter interactive widgets for Jupyter Notebook" +optional = false +python-versions = ">=3.7" +files = [ + {file = "widgetsnbextension-4.0.11-py3-none-any.whl", hash = "sha256:55d4d6949d100e0d08b94948a42efc3ed6dfdc0e9468b2c4b128c9a2ce3a7a36"}, + {file = "widgetsnbextension-4.0.11.tar.gz", hash = "sha256:8b22a8f1910bfd188e596fe7fc05dcbd87e810c8a4ba010bdb3da86637398474"}, +] + [[package]] name = "xarray" version = "2024.5.0" @@ -4302,4 +4363,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more [metadata] lock-version = "2.0" python-versions = ">=3.9, <4.0" -content-hash = "f57d757affff5478d7d54e7d4c3a4388d6059d661269eb002822c3f73e2dabf9" +content-hash = "d5dc607682c3625df46fbf2132a55610aa9f22b9ed0a6174d7b5f089b55c0ccc" diff --git a/pyproject.toml b/pyproject.toml index 2d4353d..a6a7622 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ xarray = {version = "*", extras = ["io", "accel"]} covdefaults = "*" ipykernel = "*" ipython = "*" +jupyter-bokeh = "*" mypy = ">=1" nbmake = "*" pandas-stubs = "*" diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt index 05fbd24..c64cb44 100644 --- a/requirements/requirements-dev.txt +++ b/requirements/requirements-dev.txt @@ -39,7 +39,7 @@ executing==2.0.1 ; python_version >= "3.9" and python_version < "4.0" fasteners==0.19 ; python_version >= "3.9" and python_version < "4.0" and sys_platform != "emscripten" fastjsonschema==2.19.1 ; python_version >= "3.9" and python_version < "4.0" fiona==1.9.6 ; python_version >= "3.9" and python_version < "4.0" -flox==0.9.7 ; python_version >= "3.9" and python_version < "4.0" +flox==0.9.8 ; python_version >= "3.9" and python_version < "4.0" fonttools==4.52.4 ; python_version >= "3.9" and python_version < "4.0" fsspec==2024.5.0 ; python_version >= "3.9" and python_version < "4.0" future==1.0.0 ; python_version >= "3.9" and python_version < "4.0" @@ -56,12 +56,15 @@ importlib-resources==6.4.0 ; python_version >= "3.9" and python_version < "3.10" iniconfig==2.0.0 ; python_version >= "3.9" and python_version < "4.0" ipykernel==6.29.4 ; python_version >= "3.9" and python_version < "4.0" ipython==8.18.1 ; python_version >= "3.9" and python_version < "4.0" +ipywidgets==8.1.3 ; python_version >= "3.9" and python_version < "4.0" jedi==0.19.1 ; python_version >= "3.9" and python_version < "4.0" jinja2==3.1.4 ; python_version >= "3.9" and python_version < "4.0" jsonschema-specifications==2023.12.1 ; python_version >= "3.9" and python_version < "4.0" jsonschema==4.22.0 ; python_version >= "3.9" and python_version < "4.0" +jupyter-bokeh==4.0.4 ; python_version >= "3.9" and python_version < "4.0" jupyter-client==8.6.2 ; python_version >= "3.9" and python_version < "4.0" jupyter-core==5.7.2 ; python_version >= "3.9" and python_version < "4.0" +jupyterlab-widgets==3.0.11 ; python_version >= "3.9" and python_version < "4.0" kiwisolver==1.4.5 ; python_version >= "3.9" and python_version < "4.0" linkify-it-py==2.0.3 ; python_version >= "3.9" and python_version < "4.0" llvmlite==0.42.0 ; python_version >= "3.9" and python_version < "4.0" @@ -138,7 +141,7 @@ pyyaml==6.0.1 ; python_version >= "3.9" and python_version < "4.0" pyzmq==26.0.3 ; python_version >= "3.9" and python_version < "4.0" referencing==0.35.1 ; python_version >= "3.9" and python_version < "4.0" regex==2024.5.15 ; python_version >= "3.9" and python_version < "4.0" -requests==2.32.2 ; python_version >= "3.9" and python_version < "4.0" +requests==2.32.3 ; python_version >= "3.9" and python_version < "4.0" rpds-py==0.18.1 ; python_version >= "3.9" and python_version < "4.0" scipy==1.13.1 ; python_version >= "3.9" and python_version < "4.0" shapely==2.0.4 ; python_version >= "3.9" and python_version < "4.0" @@ -162,6 +165,7 @@ watchdog==4.0.1 ; python_version >= "3.9" and python_version < "4.0" wcwidth==0.2.13 ; python_version >= "3.9" and python_version < "4.0" webencodings==0.5.1 ; python_version >= "3.9" and python_version < "4.0" webob==1.8.7 ; python_version >= "3.9" and python_version < "3.10" +widgetsnbextension==4.0.11 ; python_version >= "3.9" and python_version < "4.0" xarray-selafin==0.1.6 ; python_version >= "3.9" and python_version < "4.0" xarray==2024.5.0 ; python_version >= "3.9" and python_version < "4.0" xarray[accel,io]==2024.5.0 ; python_version >= "3.9" and python_version < "4.0" diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 6a5599c..fdffb21 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -24,7 +24,7 @@ distributed==2024.5.1 ; python_version >= "3.9" and python_version < "4.0" docopt==0.6.2 ; python_version < "3.10" and python_version >= "3.9" fasteners==0.19 ; python_version >= "3.9" and python_version < "4.0" and sys_platform != "emscripten" fiona==1.9.6 ; python_version >= "3.9" and python_version < "4.0" -flox==0.9.7 ; python_version >= "3.9" and python_version < "4.0" +flox==0.9.8 ; python_version >= "3.9" and python_version < "4.0" fonttools==4.52.4 ; python_version >= "3.9" and python_version < "4.0" fsspec==2024.5.0 ; python_version >= "3.9" and python_version < "4.0" future==1.0.0 ; python_version >= "3.9" and python_version < "4.0" @@ -78,7 +78,7 @@ python-dateutil==2.9.0.post0 ; python_version >= "3.9" and python_version < "4.0 pytz==2024.1 ; python_version >= "3.9" and python_version < "4.0" pyviz-comms==3.0.2 ; python_version >= "3.9" and python_version < "4.0" pyyaml==6.0.1 ; python_version >= "3.9" and python_version < "4.0" -requests==2.32.2 ; python_version >= "3.9" and python_version < "4.0" +requests==2.32.3 ; python_version >= "3.9" and python_version < "4.0" scipy==1.13.1 ; python_version >= "3.9" and python_version < "4.0" shapely==2.0.4 ; python_version >= "3.9" and python_version < "4.0" six==1.16.0 ; python_version >= "3.9" and python_version < "4.0" diff --git a/tests/api_test.py b/tests/api_test.py index c482ad0..e3f8fc2 100644 --- a/tests/api_test.py +++ b/tests/api_test.py @@ -9,17 +9,19 @@ from thalassa import normalization ADCIRC_NC = DATA_DIR / "fort.63.nc" -SELAFIN = DATA_DIR / "iceland.slf" +SELAFIN1 = DATA_DIR / "r2d_malpasset-char_p2.slf" +SELAFIN2 = DATA_DIR / "r2d.V1P3.slf" @pytest.mark.parametrize( - "file,variable", + "file,variable,crs", [ - pytest.param(ADCIRC_NC, "zeta"), - pytest.param(SELAFIN, "S"), + pytest.param(ADCIRC_NC, "zeta", 4326), + pytest.param(SELAFIN1, "S", None), + pytest.param(SELAFIN2, "HAUTEUR_HM0", 4326), ], ) -def test_main_api(file, variable): - ds = api.open_dataset(file) +def test_main_api(file, variable, crs): + ds = api.open_dataset(file, source_crs=crs) assert normalization.is_generic(ds) # Create objects @@ -37,7 +39,7 @@ def test_main_api(file, variable): hv.render(tap_ts, backend="bokeh") hv.render(wireframe, backend="bokeh") - assert isinstance(nodes, gv.Points) + assert isinstance(nodes, (gv.Points, hv.Points)) assert isinstance(pointer_ts, hv.DynamicMap) assert isinstance(raster, hv.DynamicMap) assert isinstance(tap_ts, hv.DynamicMap) @@ -45,16 +47,17 @@ def test_main_api(file, variable): @pytest.mark.parametrize( - "file,variable", + "file,variable,crs", [ - pytest.param(ADCIRC_NC, "zeta"), - pytest.param(SELAFIN, "S"), + pytest.param(ADCIRC_NC, "zeta", 4326), + pytest.param(SELAFIN1, "S", None), + pytest.param(SELAFIN2, "HAUTEUR_HM0", 4326), ], ) -def test_create_trimesh(file, variable): - ds = api.open_dataset(file) +def test_create_trimesh(file, variable, crs): + ds = api.open_dataset(file, source_crs=crs) trimesh = api.create_trimesh(ds, variable=variable) - assert isinstance(trimesh, gv.TriMesh) + assert isinstance(trimesh, (gv.TriMesh, hv.TriMesh)) def test_get_tiles(): diff --git a/tests/data/iceland.slf b/tests/data/iceland.slf deleted file mode 100644 index caefc23..0000000 Binary files a/tests/data/iceland.slf and /dev/null differ diff --git a/tests/data/r2d.V1P3.slf b/tests/data/r2d.V1P3.slf new file mode 100644 index 0000000..0f61852 Binary files /dev/null and b/tests/data/r2d.V1P3.slf differ diff --git a/tests/data/r2d_malpasset-char_p2.slf b/tests/data/r2d_malpasset-char_p2.slf new file mode 100644 index 0000000..0dec9b8 Binary files /dev/null and b/tests/data/r2d_malpasset-char_p2.slf differ diff --git a/tests/normalization_test.py b/tests/normalization_test.py index ca13b7e..07031a9 100644 --- a/tests/normalization_test.py +++ b/tests/normalization_test.py @@ -12,7 +12,8 @@ "ds,expected_fmt", [ pytest.param(api.open_dataset(DATA_DIR / "fort.63.nc", normalize=False), THALASSA_FORMATS.ADCIRC, id="ADCIRC"), - pytest.param(api.open_dataset(DATA_DIR / "iceland.slf", normalize=False), THALASSA_FORMATS.TELEMAC, id="TELEMAC"), + pytest.param(api.open_dataset(DATA_DIR / "r2d_malpasset-char_p2.slf", normalize=False, source_crs=4326), THALASSA_FORMATS.TELEMAC, id="TELEMAC"), + pytest.param(api.open_dataset(DATA_DIR / "r2d.V1P3.slf", normalize=False), THALASSA_FORMATS.TELEMAC, id="TELEMAC"), pytest.param(xr.Dataset(), THALASSA_FORMATS.UNKNOWN, id="Unknown"), ], ) @@ -25,7 +26,8 @@ def test_infer_format(ds, expected_fmt): "path,expected", [ pytest.param(DATA_DIR / "fort.63.nc", True, id="ADCIRC"), - pytest.param(DATA_DIR / "iceland.slf", True, id="TELEMAC"), + pytest.param(DATA_DIR / "r2d_malpasset-char_p2.slf", True, id="TELEMAC"), + pytest.param(DATA_DIR / "r2d.V1P3.slf", True, id="TELEMAC"), pytest.param(__file__, False, id="Unknown"), ], ) diff --git a/thalassa/api.py b/thalassa/api.py index 051758b..7a9055c 100644 --- a/thalassa/api.py +++ b/thalassa/api.py @@ -59,6 +59,7 @@ def _resolve_ranges(x_range: tuple[float, float] | None, y_range: tuple[float, f def open_dataset( path: str | os.PathLike[str], normalize: bool = True, + source_crs: int = 4326, **kwargs: dict[str, T.Any], ) -> xarray.Dataset: """ @@ -86,6 +87,7 @@ def open_dataset( path: The path to the dataset file (netCDF, zarr, grib) normalize: Boolean flag indicating whether the dataset should be converted/normalized to the "Thalassa schema". Normalization is currently only supported for ``SCHISM``, ``TELEMAC``, and ``ADCIRC`` netcdf files. + source_crs: The coordinate system of the dataset (default is WGS84) kwargs: The ``kwargs`` are being passed through to ``xarray.open_dataset``. """ @@ -99,7 +101,7 @@ def open_dataset( with warnings.catch_warnings(record=True): ds = xr.open_dataset(path, **(default_kwargs | kwargs)) if normalize: - ds = normalization.normalize(ds) + ds = normalization.normalize(ds, source_crs=source_crs) return ds @@ -127,10 +129,11 @@ def create_trimesh( If a trimesh object is passed, then return it immediately. variable: The data variable we want to visualize """ + import holoviews as hv import geoviews as gv from cartopy import crs - if isinstance(ds_or_trimesh, gv.TriMesh): + if isinstance(ds_or_trimesh, (gv.TriMesh, hv.TriMesh)): # This is already a trimesh, nothing to do return ds_or_trimesh else: @@ -142,19 +145,27 @@ def create_trimesh( columns.append(variable) points_df = ds[columns].to_dataframe() # Convert the data to Google Mercator. This makes interactive usage faster - transformer = _get_transformer(from_crs="EPSG:4326", to_crs="EPSG:3857") - tlon, tlat = transformer.transform(points_df.lon, points_df.lat) - points_df = points_df.assign(lon=tlon, lat=tlat) - # Create the geoviews object - kwargs = dict(data=points_df, kdims=["lon", "lat"], crs=crs.GOOGLE_MERCATOR) - if variable: - kwargs["vdims"] = [variable] - points_gv = gv.Points(**kwargs) - # Create the trimesh - if variable: - trimesh = gv.TriMesh((ds.triface_nodes.data, points_gv), name=variable) + if ds.attrs["source_crs"]: + transformer = _get_transformer(from_crs=ds.attrs["source_crs"], to_crs="EPSG:3857") + tlon, tlat = transformer.transform(points_df.lon, points_df.lat) + points_df = points_df.assign(lon=tlon, lat=tlat) + # Create the geoviews object + kwargs = dict(data=points_df, kdims=["lon", "lat"], crs=crs.GOOGLE_MERCATOR) + if variable: + kwargs["vdims"] = [variable] + points_gv = gv.Points(**kwargs) + # Create the trimesh + if variable: + trimesh = gv.TriMesh((ds.triface_nodes.data, points_gv), name=variable) + else: + trimesh = gv.TriMesh((ds.triface_nodes.data, points_gv)) else: - trimesh = gv.TriMesh((ds.triface_nodes.data, points_gv)) + points_gv = hv.Points(data=points_df, kdims=["lon", "lat"]) + # Create the trimesh + if variable: + trimesh = hv.TriMesh((ds.triface_nodes.data, points_gv), name=variable) + else: + trimesh = hv.TriMesh((ds.triface_nodes.data, points_gv)) return trimesh @@ -184,19 +195,39 @@ def get_nodes( """Return a ``DynamicMap`` with the nodes of the mesh.""" from cartopy import crs import geoviews as gv + import holoviews as hv trimesh = create_trimesh(ds_or_trimesh) + # determine if there is a crs transformation or not + if isinstance(ds_or_trimesh, (gv.TriMesh, hv.TriMesh)): + if isinstance(ds_or_trimesh, gv.TriMesh): + crs_transform = True + else: + crs_transform = False + else: + if ds_or_trimesh.attrs["source_crs"]: + crs_transform = True + else: + crs_transform = False + kwargs: dict[str, T.Any] = {} _resolve_ranges(x_range=x_range, y_range=y_range, kwargs=kwargs) tools = ["crosshair"] if hover: tools.append("hover") - points = gv.Points( - trimesh.nodes.data.rename(columns={"index": "node"}), - kdims=["lon", "lat"], - vdims=["node"], - crs=crs.GOOGLE_MERCATOR, - ) + if crs_transform: + points = gv.Points( + trimesh.nodes.data.rename(columns={"index": "node"}), + kdims=["lon", "lat"], + vdims=["node"], + crs=crs.GOOGLE_MERCATOR, + ) + else: + points = hv.Points( + trimesh.nodes.data.rename(columns={"index": "node"}), + kdims=["lon", "lat"], + vdims=["node"], + ) return points.opts(tools=tools, size=size, title=title, color="green") @@ -274,7 +305,6 @@ def get_hover(variable: str) -> bokeh.models.HoverTool: return hover - def _get_stream_timeseries( ds: xarray.Dataset, variable: str, @@ -341,7 +371,7 @@ def callback(x: float, y: float) -> holoviews.Curve: def get_station_timeseries( stations: xarray.Dataset, pins: geoviews.DynamicMap, -) -> holoviews.DynamicMap: # pragma: no cover +) -> holoviews.DynamicMap: # pragma: no cover import holoviews as hv import pandas as pd diff --git a/thalassa/normalization.py b/thalassa/normalization.py index df2d306..3adeacc 100644 --- a/thalassa/normalization.py +++ b/thalassa/normalization.py @@ -184,7 +184,7 @@ def normalize_telemac(ds: xarray.Dataset) -> xarray.Dataset: # TELEMAC output uses one-based indices for `face_nodes` # Let's ensure that we use zero-based indices everywhere. - ds[CONNECTIVITY] = ((FACE_DIM, VERTICE_DIM), ds.attrs['ikle2'] - 1) + ds[CONNECTIVITY] = ((FACE_DIM, VERTICE_DIM), ds.attrs["ikle2"] - 1) return ds @@ -227,7 +227,7 @@ def normalize_adcirc(ds: xarray.Dataset) -> xarray.Dataset: } -def normalize(ds: xarray.Dataset) -> xarray.Dataset: +def normalize(ds: xarray.Dataset, source_crs: int = 4326) -> xarray.Dataset: """ Normalize the `dataset` i.e. convert it to the "Thalassa Schema". @@ -243,12 +243,14 @@ def normalize(ds: xarray.Dataset) -> xarray.Dataset: Parameters: ds: The dataset we want to convert. + source_crs: The coordinate system of the dataset (default is WGS84) """ logger.debug("Dataset normalization: Started") fmt = infer_format(ds) normalizer_func = NORMALIZE_DISPATCHER[fmt] normalized_ds = normalizer_func(ds) + normalized_ds.attrs["source_crs"] = source_crs # Handle quad elements # Splitting quad elements to triangles, means that the number of faces increases # There are two options: