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
64 changes: 64 additions & 0 deletions .github/workflows/build-qgis-plugin.yml
Original file line number Diff line number Diff line change
@@ -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 }}
24 changes: 24 additions & 0 deletions .github/workflows/draft-pdf.yml
Original file line number Diff line number Diff line change
@@ -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
Binary file added assets/Figure_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Figure_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Figure_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
139 changes: 139 additions & 0 deletions paper.bib
Original file line number Diff line number Diff line change
@@ -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}
}
89 changes: 89 additions & 0 deletions paper.md
Original file line number Diff line number Diff line change
@@ -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 <https://github.com/GeoOcean/BlueMesh2D>. 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
Loading