diff --git a/.github/workflows/build-qgis-plugin.yml b/.github/workflows/build-qgis-plugin.yml new file mode 100644 index 0000000..70fe070 --- /dev/null +++ b/.github/workflows/build-qgis-plugin.yml @@ -0,0 +1,64 @@ +name: Build QGIS Plugin Zip + +on: + push: + tags: + - "v*" + - "qgis-v*" + workflow_dispatch: + +jobs: + build-qgis-plugin: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Read plugin version from metadata.txt + id: meta + run: | + version=$(sed -n 's/^version=//p' bluemesh2d_qgis/metadata.txt | head -1 | tr -d '[:space:]') + if [ -z "$version" ]; then + echo "::error::no version= line in bluemesh2d_qgis/metadata.txt" + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "zip=bluemesh2d_qgis-$version.zip" >> "$GITHUB_OUTPUT" + + - name: Build plugin zip + run: | + git archive --format=zip \ + --prefix=bluemesh2d_qgis/ \ + -o "${{ steps.meta.outputs.zip }}" \ + HEAD:bluemesh2d_qgis + + - name: Validate zip layout + run: | + python - <<'PY' + import zipfile + zf = zipfile.ZipFile("${{ steps.meta.outputs.zip }}") + names = zf.namelist() + roots = {n.split("/")[0] for n in names} + assert roots == {"bluemesh2d_qgis"}, f"expected one top-level dir, got {roots}" + for required in ("bluemesh2d_qgis/metadata.txt", "bluemesh2d_qgis/__init__.py"): + assert required in names, f"missing {required}" + assert not [n for n in names if "__pycache__" in n], "__pycache__ in zip" + src = zf.read("bluemesh2d_qgis/__init__.py").decode() + assert "def classFactory" in src, "no classFactory entry point" + print(f"OK: {len(names)} entries") + PY + + - name: Upload as workflow artifact + uses: actions/upload-artifact@v4 + with: + name: bluemesh2d_qgis-${{ steps.meta.outputs.version }} + path: ${{ steps.meta.outputs.zip }} + + - name: Attach to GitHub Release + if: github.ref_type == 'tag' + uses: softprops/action-gh-release@v2 + with: + files: ${{ steps.meta.outputs.zip }} diff --git a/.github/workflows/draft-pdf.yml b/.github/workflows/draft-pdf.yml new file mode 100644 index 0000000..a319c6c --- /dev/null +++ b/.github/workflows/draft-pdf.yml @@ -0,0 +1,24 @@ +name: Draft PDF +on: [push] + +jobs: + paper: + runs-on: ubuntu-latest + name: Paper Draft + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build draft PDF + uses: openjournals/openjournals-draft-action@master + with: + journal: joss + # This should be the path to the paper within your repo. + paper-path: paper.md + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: paper + # This is the output path where Pandoc will write the compiled + # PDF. Note, this should be the same directory as the input + # paper.md + path: paper.pdf \ No newline at end of file diff --git a/assets/Figure_1.png b/assets/Figure_1.png new file mode 100644 index 0000000..586e99e Binary files /dev/null and b/assets/Figure_1.png differ diff --git a/assets/Figure_2.png b/assets/Figure_2.png new file mode 100644 index 0000000..09a22cc Binary files /dev/null and b/assets/Figure_2.png differ diff --git a/assets/Figure_3.png b/assets/Figure_3.png new file mode 100644 index 0000000..caa93f4 Binary files /dev/null and b/assets/Figure_3.png differ diff --git a/paper.bib b/paper.bib new file mode 100644 index 0000000..3a125dc --- /dev/null +++ b/paper.bib @@ -0,0 +1,139 @@ +@phdthesis{Engwirda2014, + author = {Engwirda, Darren}, + title = {Locally optimal Delaunay-refinement and optimisation-based mesh generation}, + school = {University of Sydney}, + year = {2014}, + url = {http://hdl.handle.net/2123/13148} +} + +@article{Geuzaine2009, + author = {Geuzaine, Christophe and Remacle, Jean-Fran{\c{c}}ois}, + title = {Gmsh: A 3-D finite element mesh generator with built-in pre- and post-processing facilities}, + journal = {International Journal for Numerical Methods in Engineering}, + volume = {79}, + number = {11}, + pages = {1309--1331}, + year = {2009}, + doi = {10.1002/nme.2579} +} + +@article{Hunt1979, + author = {Hunt, J. N.}, + title = {Direct solution of wave dispersion equation}, + journal = {Journal of the Waterway, Port, Coastal and Ocean Division}, + volume = {105}, + number = {4}, + pages = {457--459}, + year = {1979} +} + +@article{Kernkamp2011, + author = {Kernkamp, H. W. J. and Van Dam, A. and Stelling, G. S. and de Goede, E. D.}, + title = {Efficient scheme for the shallow water equations on unstructured grids with application to the Continental Shelf}, + journal = {Ocean Dynamics}, + volume = {61}, + number = {8}, + pages = {1175--1188}, + year = {2011}, + doi = {10.1007/s10236-011-0423-6} +} + +@article{Persson2006, + author = {Persson, Per-Olof}, + title = {Mesh size functions for implicit geometries and PDE-based gradient limiting}, + journal = {Engineering with Computers}, + volume = {22}, + number = {2}, + pages = {95--109}, + year = {2006}, + doi = {10.1007/s00366-006-0014-1} +} + +@article{Persson2004, + author = {Persson, Per-Olof and Strang, Gilbert}, + title = {A simple mesh generator in {MATLAB}}, + journal = {SIAM Review}, + volume = {46}, + number = {2}, + pages = {329--345}, + year = {2004}, + doi = {10.1137/S0036144503429121} +} + +@misc{QGIS2024, + author = {{QGIS Development Team}}, + title = {{QGIS} Geographic Information System}, + howpublished = {Open Source Geospatial Foundation Project}, + year = {2024}, + url = {https://qgis.org} +} + +@article{Roberts2019, + author = {Roberts, Keith J. and Pringle, William J. and Westerink, Joannes J.}, + title = {{OceanMesh2D} 1.0: {MATLAB}-based software for two-dimensional unstructured mesh generation in coastal ocean modeling}, + journal = {Geoscientific Model Development}, + volume = {12}, + number = {5}, + pages = {1847--1868}, + year = {2019}, + doi = {10.5194/gmd-12-1847-2019} +} + +@article{Roberts2021, + author = {Roberts, Keith J. and dos Santos Gioria, Rafael and Pringle, William J.}, + title = {{SeismicMesh}: Triangular meshing for seismology}, + journal = {Journal of Open Source Software}, + volume = {6}, + number = {57}, + pages = {2687}, + year = {2021}, + doi = {10.21105/joss.02687} +} + +@incollection{Shewchuk1996, + author = {Shewchuk, Jonathan Richard}, + title = {Triangle: Engineering a 2{D} quality mesh generator and {Delaunay} triangulator}, + booktitle = {Applied Computational Geometry Towards Geometric Engineering}, + pages = {203--222}, + year = {1996}, + doi = {10.1007/BFb0014497} +} + +@misc{Deltares_MeshKernel, + author = {{Deltares}}, + title = {{MeshKernel}}, + howpublished = {Computer software}, + url = {https://github.com/Deltares/MeshKernel} +} + +@misc{EMODnet2022, + author = {{EMODnet Bathymetry Consortium}}, + title = {{EMODnet} Digital Bathymetry ({DTM} 2022)}, + year = {2022}, + doi = {10.12770/ff3aff8a-cff1-44a3-a2c8-1910bf109f85} +} + +@misc{Lambrechts2021, + author = {Lambrechts, Jonathan}, + title = {seamsh: ocean mesh generation}, + howpublished = {Computer software}, + year = {2021}, + url = {https://git.immc.ucl.ac.be/jlambrechts/seamsh} +} + +@techreport{Mani2021, + author = {Mani, Soroosh and Calzada, Jaime R. and Moghimi, Saeed and Zhang, Y. Joseph and Myers, Edward and Pe'eri, Shachak}, + title = {{OCSMesh}: a data-driven automated unstructured mesh generation software for coastal ocean modeling}, + institution = {Coast Survey Development Laboratory, NOAA}, + number = {NOAA Technical Memorandum NOS CS 47}, + year = {2021}, + doi = {10.25923/csba-m072} +} + +@misc{Roberts2020, + author = {Roberts, Keith J.}, + title = {oceanmesh: automatic coastal ocean mesh generation in {Python} and {C++}}, + howpublished = {Computer software}, + year = {2020}, + url = {https://github.com/CHLNDDEV/oceanmesh} +} diff --git a/paper.md b/paper.md new file mode 100644 index 0000000..5e9a2b1 --- /dev/null +++ b/paper.md @@ -0,0 +1,89 @@ +--- +title: 'BlueMesh2D: a Python, QGIS-interfaced tool for unstructured coastal mesh generation' +tags: + - Python + - QGIS + - GIS + - unstructured mesh generation + - coastal engineering + - hydrodynamic modeling +authors: + - name: Etienne Faugere + orcid: 0009-0009-4082-3953 + affiliation: 1 + - name: Javier Tausia Hoyal + orcid: 0000-0002-2299-2915 + affiliation: 1 + - name: Beatriz Pérez Díaz + orcid: 0000-0002-1987-2605 + affiliation: "1, 2" + - name: Sonia Castanedo Barcena + orcid: 0000-0002-5016-2468 + affiliation: 1 +affiliations: + - name: Geomatics and Ocean Engineering Group, Departamento de Ciencias y Técnicas del Agua y del Medio Ambiente, Universidad de Cantabria, Avda. de los Castros 44, 39005 Santander, Spain + index: 1 + - name: Department of Applied Mathematics and Computer Science, Universidad de Cantabria, Avda. de los Castros 44, 39005 Santander, Spain + index: 2 +date: 03 August 2026 +bibliography: paper.bib +nocite: | + @QGIS2024, @Deltares_MeshKernel +--- + +# Summary + +Coastal numerical models require high-quality unstructured meshes, where element size must grade smoothly from metres nearshore to kilometres offshore. BlueMesh2D is an open-source, pure-Python mesh generator driven through a graphical QGIS interface (distributed as a Processing plugin) that produces simulation-ready unstructured meshes directly from a bathymetry raster, without scripting. It combines Delaunay and Frontal-Delaunay refinement [@Engwirda2014] with DistMesh-style optimization [@Persson2004], a Delft3D-FM orthogonalization pass, and a Lipschitz-limited size function [@Persson2006]. BlueMesh2D contributes an integrated coastal meshing workflow combining existing meshing algorithms with GIS-native editing, coastal-specific sizing functions, boundary classification and direct export to operational hydrodynamic models. + +# Statement of Need + +While coastal mesh generatos are available, each comes with its own set of trade-offs. OceanMesh2D [@Roberts2019] is one of the most established: resolution is driven by geometric and topo-bathymetric size functions, vertices are placed by a force-balance algorithm, and worst-case triangle quality is improved topologically. It runs in MATLAB, however, which restricts its reach. Several open Python descendants lift that restriction, among them oceanmesh [@Roberts2020], OCSMesh [@Mani2021], SeismicMesh [@Roberts2021] and seamsh [@Lambrechts2021], but they remain libraries: the domain, the sizing law and the export step are assembled in a script. Most are also thin layers over a general-purpose kernel, with OCSMesh delegating the meshing itself to JIGSAW [@Engwirda2014] and seamsh wrapping Gmsh [@Geuzaine2009], alongside Triangle [@Shewchuk1996] in other toolchains. These kernels are fast and well tested, but they are compiled C/C++ libraries, which is a real constraint inside a GIS, where the plugin must install into an existing Python environment that the user does not control. Commercial packages and solver-specific grid editors do offer a graphical interface, but at the cost of licensing and portability. QGIS, meanwhile, already holds the bathymetry, the coastline and the CRS (Coordinate Reference System) definitions, yet it cannot generate a mesh from them, so data are prepared in a GIS, exported, meshed elsewhere and re-imported for inspection. BlueMesh2D closes that loop by enabling users to generate highly complex coastal meshes directly within QGIS—providing a streamlined, no-code workflow in a fully open-source environment. + +# Implementation + +The meshing kernel is a Python translation and extension of the MESH2D/JIGSAW methodology [@Engwirda2014]. From a boundary (PSLG) and a size function h(x, y) it builds a constrained Delaunay triangulation. Triangle [@Shewchuk1996] is used opportunistically where the environment already provides it, but the kernel does not depend on it: a conforming-Delaunay construction in SciPy serves as a fallback, so no compilation step is required at install time. The triangulation is then refined either by classical Delaunay refinement or by the locally optimal Frontal-Delaunay scheme, both of which guarantee termination and bound worst-case element quality. A DistMesh-style hill-climbing optimisation [@Persson2004] relaxes vertex positions and applies local topological operations under a monotone quality criterion. For finite-volume flexible-mesh solvers, an optional orthogonalisation pass ported from Deltares MeshKernel bounds the edge-to-dual (flow-link) angle and removes short flow links, enforcing the near-orthogonality that D-Flow FM requires [@Kernkamp2011]. + +Element size can follow a depth polynomial, a wavelength law L(T, d)/N [@Hunt1979], bathymetric slope, a constant, or a user expression. Here L is the local linear wavelength, T the wave period chosen as reference, d the local water depth taken from the bathymetry, and N the number of elements requested per wavelength, so that shallower water yields shorter waves and therefore smaller elements. Each rule is floored, capped and gradient-limited by a Lipschitz smoothing pass [@Persson2006] so that size grades smoothly across the domain. Bathymetry is sampled at true raster cell centres, honouring the GeoTIFF pixel-is-area / pixel-is-point convention so that the extracted coastline and node depths register correctly against the source grid. Vertices flagged as fixed are preserved exactly throughout refinement and smoothing. + +The meshing kernel is covered by an automated pytest suite: regression tests compare refinement and smoothing output against stored reference meshes, geometric-validity tests assert finite coordinates and non-degenerate triangles, and dedicated tests check that orthogonalization improves flow-link orthogonality while holding fixed vertices in place. Each pipeline stage is also exercised end-to-end on synthetic and real bathymetry, across the three major platforms and multiple QGIS releases. + +On first load the plugin detects and installs missing dependencies into the running QGIS interpreter (via a plugin-managed virtual environment on externally managed system Pythons), so no manual setup is required on Linux, Windows or macOS. + +# Functionality + +BlueMesh2D exposes the kernel as six QGIS Processing algorithms that chain through editable layers: + +- Domain extraction: contours the raster to a coastline and a clip domain (a user extent polygon or the buffered raster extent), intersects them in a local metric (UTM) CRS, repairs invalid geometry, and flags domain-boundary and coastline-intersection vertices as fixed. An auxiliary tool sets the fixed/free flag for every vertex inside an area drawn on the map or selected from a polygon layer, optionally editing the polygon in place so large groups of vertices can be pinned or released in one operation. → editable water polygon. +- Element-size function: evaluates the chosen sizing law with optional local "detail" refinement and gradient limiting. → size raster. +- Boundary resampling: re-parameterizes each ring at arc-length spacing weighted by 1/h, resampling arc-by-arc between fixed points so they survive exactly, and prunes spikes. → editable boundary-edge layer. +- Triangulation and optimization: constrained Delaunay refinement, smoothing, optional orthogonalization, and bathymetry sampling onto nodes. → UGRID mesh layer. +- Boundary-condition classification: traces boundary loops and classifies each segment as open/closed/island by depth. → editable btype line layer. +- Export: writes UGRID NetCDF (with optional metadata override), Delft3D-FM open-boundary files (`.pli`/`.bc`), or ADCIRC `.grd`, snapping boundary vertices back to mesh nodes so edits are honoured. + +At each stage the user may edit the output before continuing (reshape the domain, change sizing parameters, move/add/delete boundary vertices, pin extra interior points, or reclassify boundary segments) and the next stage rebuilds from whatever remains. This human-in-the-loop, layer-based design is the tool's central usability contribution and its main point of difference from script-driven coastal meshers. + +# Examples + +As a representative application, we mesh the Bay of Santander (northern Spain) from EMODnet data [@EMODnet2022]: the terrestrial cells define the coastline and the marine cells supply the depth on a grid of 1/16 × 1/16 arc minute of longitude and latitude (ca. 115 × 115 m). + +The water polygon is extracted at the coastline level, cut by a buffered circular extent, with cut and coastline-junction vertices flagged as fixed and user-defined fixed points pinning features of interest inside the estuary. A depth-based size function grades from roughly 20 m in the inner harbour to about 960 m offshore; the boundary is resampled to that field and meshed, with EMODnet bathymetry interpolated onto the nodes (bed elevation about −57 to +17 m) and an optional orthogonalization pass for Delft3D-FM. Boundary points are then classified as open, closed or island by depth and exported as UGRID NetCDF with the Delft3D-FM open-boundary files. + +The triangulation runs from the QGIS graphical interface on a standard laptop in about 20 seconds for a 36,000-element mesh, and every intermediate layer can be inspected and hand-edited before the next stage, so the domain outline, fixed points, element-size parameters, boundary geometry, and boundary types are all corrected in place without leaving QGIS. The workflow shown in Figure 1 is reproduced step by step in a tutorial provided in the docs/ folder of the repository. + +![The BlueMesh2D interface in QGIS. The Processing Toolbox (right) lists the six numbered algorithm groups; the map canvas shows the generated unstructured mesh over the Bay of Santander, refined inside the estuary and coarsening offshore, with the interpolated bed elevation, the user-defined fixed points, a highlighted detail region in the port, and the editable open, closed (land), and island boundary layers.](assets/Figure_1.png){ width=100% } + +The workflow scales to regional domains. Figure 2 shows an eastern Gulf of Mexico mesh with detail polygons refining the bays around the validation tide gauges, TG1 and TG2, and Figure 3 a northwest European shelf mesh where the slope criterion refines the continental slope. Both were built through the same six QGIS algorithms as the Santander case, without scripting. + +![Eastern Gulf of Mexico mesh. (a) domain and bed elevation; (b, c) detail areas, refined by the detail-polygon minimum size.](assets/Figure_2.png){ width=100% } + +![Northwest European shelf mesh coloured by bed elevation. The depth law resolves the shelf; the slope criterion adds the refinement band along the continental slope.](assets/Figure_3.png){ width=70% } + +# Availability + +BlueMesh2D is open-source software released under the GNU GPL v3. The source code, the QGIS plugin and documentation are available on GitHub at . The core library is distributed on PyPI as `bluemesh2d` (`pip install bluemesh2d`), and the graphical interface through the official QGIS Plugin Repository, installable directly from the QGIS Plugin Manager. + +# Acknowledgements + +BlueMesh2D builds on the MESH2D methodology of D. Engwirda and the mesh-orthogonalization routines of the Deltares MeshKernel library, whose open-source availability made this work possible. Development was carried out at the GeoOcean group, Universidad de Cantabria. + +# References