From 4c347f6df64cbe702b6a7e296a92a8d7ba11b94c Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 26 Nov 2025 13:44:21 +0200 Subject: [PATCH 1/4] Add option for vertical CRS & automatically copy files from PROJ --- Mergin/project_settings_widget.py | 37 ++++++++++++- Mergin/ui/ui_project_config.ui | 92 ++++++++++++++++++++++++++----- Mergin/utils.py | 18 ++++++ 3 files changed, 132 insertions(+), 15 deletions(-) diff --git a/Mergin/project_settings_widget.py b/Mergin/project_settings_widget.py index 8936ab00..fd7f12bf 100644 --- a/Mergin/project_settings_widget.py +++ b/Mergin/project_settings_widget.py @@ -4,6 +4,7 @@ import json import os import typing +import re from qgis.PyQt import uic from qgis.PyQt.QtGui import QIcon, QColor from qgis.PyQt.QtCore import Qt @@ -16,8 +17,15 @@ QgsFeatureRequest, QgsExpression, QgsMapLayer, + QgsCoordinateReferenceSystem, +) +from qgis.gui import ( + QgsOptionsWidgetFactory, + QgsOptionsPageWidget, + QgsColorButton, + QgsCoordinateReferenceSystemProxyModel, + QgsProjectionSelectionWidget, ) -from qgis.gui import QgsOptionsWidgetFactory, QgsOptionsPageWidget, QgsColorButton from .attachment_fields_model import AttachmentFieldsModel from .utils import ( mm_symbol_path, @@ -31,6 +39,8 @@ invalid_filename_character, qvariant_to_string, escape_html_minimal, + copy_datum_shift_grid, + project_grids_directory, ) ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_project_config.ui") @@ -118,6 +128,13 @@ def __init__(self, parent=None): idx = self.cmb_sort_method.findData(mode) if ok else 1 self.cmb_sort_method.setCurrentIndex(idx) + self.cmb_vertical_crs.setFilters(QgsCoordinateReferenceSystemProxyModel.FilterVertical) + vcrs_def, ok = QgsProject.instance().readEntry("Mergin", "TargetVerticalCRS") + vertical_crs = QgsCoordinateReferenceSystem.fromWkt(vcrs_def) if ok else QgsCoordinateReferenceSystem.fromEpsgId(5773) #EGM96 geoid model + self.cmb_vertical_crs.setCrs(vertical_crs) + self.cmb_vertical_crs.setOptionVisible(QgsProjectionSelectionWidget.CurrentCrs, True) + self.cmb_vertical_crs.setDialogTitle("Target Vertical CRS") + self.local_project_dir = mergin_project_local_path() if self.local_project_dir: @@ -309,6 +326,22 @@ def setup_map_sketches(self): # create a new layer and add it as a map sketches layer create_map_sketches_layer(QgsProject.instance().absolutePath()) + def package_vcrs_file(self, vertical_crs): + """ + Get the grid shift file name from proj definition and copy it to project proj folder. We do this only for vertical CRS different than EGM96. + """ + if vertical_crs != QgsCoordinateReferenceSystem.fromEpsgId(5773): + # search for required file name + result = re.search("=.*\.tif ", vertical_crs.toProj()) + if result is not None: + # sanitize matched result + vcrs_file = result.group()[1:-1] + grids_directory = os.path.join(mergin_project_local_path(), "proj") + if grids_directory is not None: + return copy_datum_shift_grid(grids_directory, vcrs_file) + return False + return True + def apply(self): QgsProject.instance().writeEntry("Mergin", "PhotoQuality", self.cmb_photo_quality.currentData()) QgsProject.instance().writeEntry("Mergin", "Snapping", self.cmb_snapping_mode.currentData()) @@ -345,10 +378,12 @@ def apply(self): expression = item.data(AttachmentFieldsModel.EXPRESSION) QgsProject.instance().writeEntry("Mergin", f"PhotoNaming/{layer_id}/{field_name}", expression) + QgsProject.instance().writeEntry("Mergin", "TargetVerticalCRS", self.cmb_vertical_crs.crs().toWkt()) QgsProject.instance().writeEntry("Mergin", "SortLayersMethod/Method", self.cmb_sort_method.currentData()) self.save_config_file() self.setup_tracking() self.setup_map_sketches() + self.package_vcrs_file(self.cmb_vertical_crs.crs()) def colors_change_state(self) -> None: """ diff --git a/Mergin/ui/ui_project_config.ui b/Mergin/ui/ui_project_config.ui index e29310cc..ec479f35 100644 --- a/Mergin/ui/ui_project_config.ui +++ b/Mergin/ui/ui_project_config.ui @@ -44,9 +44,9 @@ 0 - -888 - 628 - 1444 + -1120 + 621 + 1678 @@ -153,9 +153,6 @@ Snapping - - - @@ -163,6 +160,9 @@ + + + @@ -352,7 +352,7 @@ - + 255 255 @@ -363,7 +363,7 @@ - + 18 24 @@ -374,7 +374,7 @@ - + 94 158 @@ -385,7 +385,7 @@ - + 87 180 @@ -396,7 +396,7 @@ - + 253 203 @@ -407,7 +407,7 @@ - + 255 156 @@ -418,7 +418,7 @@ - + 255 143 @@ -471,6 +471,64 @@ + + + + Vertical CRS + + + + + + Choose which vertical CRS will be used for elevation calculation. By default the mobile application uses EGM96_15 geoid model. + + + true + + + true + + + + + + + QLayout::SetDefaultConstraint + + + + + + 0 + 0 + + + + Vertical CRS + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + + + + + + @@ -493,7 +551,7 @@ Photo sketching lets mobile app users draw freehand annotations directly on captured photos. This feature is in Preview: it works, but may still have some issues. - true + true @@ -531,6 +589,12 @@
qgsexpressionlineedit.h
1 + + QgsProjectionSelectionWidget + QWidget +
qgsprojectionselectionwidget.h
+ 1 +
chk_sync_enabled diff --git a/Mergin/utils.py b/Mergin/utils.py index 812e5f64..06be7684 100644 --- a/Mergin/utils.py +++ b/Mergin/utils.py @@ -1279,6 +1279,24 @@ def copy_datum_shift_grids(grids_dir): return missed_files +def copy_datum_shift_grid(grids_dir, grid_file): + """ + Copies datum shift grid file specified by name inside MerginMaps "proj" directory. + """ + os.makedirs(grids_dir, exist_ok=True) + copy_ok = False + for p in QgsProjUtils.searchPaths(): + src = os.path.join(p, grid_file) + if not os.path.exists(src): + continue + + dst = os.path.join(grids_dir, grid_file) + if not os.path.exists(dst): + shutil.copy(src, dst) + copy_ok = True + break + + return copy_ok def project_grids_directory(mp): """ From d4f9a69d2fd01fa8c19629f74c9c700e91fe2241 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 26 Nov 2025 18:02:14 +0200 Subject: [PATCH 2/4] Change input UI --- Mergin/project_settings_widget.py | 30 +++++++- Mergin/ui/ui_project_config.ui | 109 +++++++++++++++++++----------- 2 files changed, 99 insertions(+), 40 deletions(-) diff --git a/Mergin/project_settings_widget.py b/Mergin/project_settings_widget.py index fd7f12bf..5754101f 100644 --- a/Mergin/project_settings_widget.py +++ b/Mergin/project_settings_widget.py @@ -7,7 +7,7 @@ import re from qgis.PyQt import uic from qgis.PyQt.QtGui import QIcon, QColor -from qgis.PyQt.QtCore import Qt +from qgis.PyQt.QtCore import Qt, QFileInfo from qgis.PyQt.QtWidgets import QFileDialog, QMessageBox from qgis.core import ( QgsProject, @@ -18,6 +18,8 @@ QgsExpression, QgsMapLayer, QgsCoordinateReferenceSystem, + QgsProjUtils, + QgsMessageLog, ) from qgis.gui import ( QgsOptionsWidgetFactory, @@ -131,9 +133,11 @@ def __init__(self, parent=None): self.cmb_vertical_crs.setFilters(QgsCoordinateReferenceSystemProxyModel.FilterVertical) vcrs_def, ok = QgsProject.instance().readEntry("Mergin", "TargetVerticalCRS") vertical_crs = QgsCoordinateReferenceSystem.fromWkt(vcrs_def) if ok else QgsCoordinateReferenceSystem.fromEpsgId(5773) #EGM96 geoid model + self.cmb_vertical_crs.crsChanged.connect(self.geoid_model_path_change_state) self.cmb_vertical_crs.setCrs(vertical_crs) self.cmb_vertical_crs.setOptionVisible(QgsProjectionSelectionWidget.CurrentCrs, True) self.cmb_vertical_crs.setDialogTitle("Target Vertical CRS") + self.btn_get_geoid_file.clicked.connect(self.get_geoid_path) self.local_project_dir = mergin_project_local_path() @@ -149,6 +153,29 @@ def __init__(self, parent=None): self.attachment_fields.selectionModel().currentChanged.connect(self.update_expression_edit) self.edit_photo_expression.expressionChanged.connect(self.expression_changed) + def geoid_model_path_change_state(self, newCRS): + if newCRS == QgsCoordinateReferenceSystem.fromEpsgId(5773): + self.label_geoid_file.hide() + self.edit_geoid_file.hide() + self.edit_geoid_file.clear() + self.btn_get_geoid_file.hide() + else: + self.label_geoid_file.show() + self.edit_geoid_file.show() + self.btn_get_geoid_file.show() + + def get_geoid_path(self): + # open the set location or user home + open_path = QFileInfo(self.edit_geoid_file.text()).absolutePath() if len(self.edit_geoid_file.text()) > 0 else os.path.expanduser("~") + abs_path = QFileDialog.getOpenFileName( + None, + "Select File", + open_path, + "Geoid Model Files (*.tif *.gtx)" + ) + if len(abs_path[0]) > 0: + self.edit_geoid_file.setText(abs_path[0]) + def get_sync_dir(self): abs_path = QFileDialog.getExistingDirectory( None, @@ -326,6 +353,7 @@ def setup_map_sketches(self): # create a new layer and add it as a map sketches layer create_map_sketches_layer(QgsProject.instance().absolutePath()) + #we could possibly first lookup if the gridfile is available with QGSProjUtils.gridsUsed()` def package_vcrs_file(self, vertical_crs): """ Get the grid shift file name from proj definition and copy it to project proj folder. We do this only for vertical CRS different than EGM96. diff --git a/Mergin/ui/ui_project_config.ui b/Mergin/ui/ui_project_config.ui index ec479f35..c54005f2 100644 --- a/Mergin/ui/ui_project_config.ui +++ b/Mergin/ui/ui_project_config.ui @@ -44,9 +44,9 @@ 0 - -1120 + -1160 621 - 1678 + 1716 @@ -476,9 +476,57 @@ Vertical CRS - - + + + + + ... + + + + + + + + + + + 0 + 0 + + + + Vertical CRS + + + + + + + + 0 + 0 + + + + Geoid model file + + + true + + + true + + + + + + + 0 + 0 + + Choose which vertical CRS will be used for elevation calculation. By default the mobile application uses EGM96_15 geoid model. @@ -490,41 +538,24 @@ - - - - QLayout::SetDefaultConstraint - - - - - - 0 - 0 - - - - Vertical CRS - - - - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - - + + + + true + + + + 175 + 0 + + + + + + + Select geoid model file + + From ec1621870a83b6074125a01cafb7d14e0de6f41e Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 26 Nov 2025 21:52:00 +0200 Subject: [PATCH 3/4] Add user picked file packaging --- Mergin/project_settings_widget.py | 26 ++++++++------------------ Mergin/utils.py | 17 ++++++----------- 2 files changed, 14 insertions(+), 29 deletions(-) diff --git a/Mergin/project_settings_widget.py b/Mergin/project_settings_widget.py index 5754101f..73197687 100644 --- a/Mergin/project_settings_widget.py +++ b/Mergin/project_settings_widget.py @@ -4,7 +4,6 @@ import json import os import typing -import re from qgis.PyQt import uic from qgis.PyQt.QtGui import QIcon, QColor from qgis.PyQt.QtCore import Qt, QFileInfo @@ -18,8 +17,6 @@ QgsExpression, QgsMapLayer, QgsCoordinateReferenceSystem, - QgsProjUtils, - QgsMessageLog, ) from qgis.gui import ( QgsOptionsWidgetFactory, @@ -41,8 +38,7 @@ invalid_filename_character, qvariant_to_string, escape_html_minimal, - copy_datum_shift_grid, - project_grids_directory, + copy_file_new, ) ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_project_config.ui") @@ -353,22 +349,16 @@ def setup_map_sketches(self): # create a new layer and add it as a map sketches layer create_map_sketches_layer(QgsProject.instance().absolutePath()) - #we could possibly first lookup if the gridfile is available with QGSProjUtils.gridsUsed()` + # we could possibly first lookup if the gridfile is available with QGSProjUtils.gridsUsed()` def package_vcrs_file(self, vertical_crs): """ - Get the grid shift file name from proj definition and copy it to project proj folder. We do this only for vertical CRS different than EGM96. + Get the grid shift file picked by user and copy it to project proj folder. We do this only for vertical CRS different than EGM96. """ - if vertical_crs != QgsCoordinateReferenceSystem.fromEpsgId(5773): - # search for required file name - result = re.search("=.*\.tif ", vertical_crs.toProj()) - if result is not None: - # sanitize matched result - vcrs_file = result.group()[1:-1] - grids_directory = os.path.join(mergin_project_local_path(), "proj") - if grids_directory is not None: - return copy_datum_shift_grid(grids_directory, vcrs_file) - return False - return True + if len(self.edit_geoid_file.text()) == 0: + return True + + project_proj_dir = os.path.join(mergin_project_local_path(), "proj") + return copy_file_new(project_proj_dir, self.edit_geoid_file.text()) def apply(self): QgsProject.instance().writeEntry("Mergin", "PhotoQuality", self.cmb_photo_quality.currentData()) diff --git a/Mergin/utils.py b/Mergin/utils.py index 06be7684..d4d2f92c 100644 --- a/Mergin/utils.py +++ b/Mergin/utils.py @@ -1279,22 +1279,17 @@ def copy_datum_shift_grids(grids_dir): return missed_files -def copy_datum_shift_grid(grids_dir, grid_file): +def copy_file_new(dst_dir, src_file): """ - Copies datum shift grid file specified by name inside MerginMaps "proj" directory. + Copies file, which doesn't exist in destination directory, specified by absolute path. """ - os.makedirs(grids_dir, exist_ok=True) + os.makedirs(dst_dir, exist_ok=True) copy_ok = False - for p in QgsProjUtils.searchPaths(): - src = os.path.join(p, grid_file) - if not os.path.exists(src): - continue - - dst = os.path.join(grids_dir, grid_file) + if os.path.exists(src_file): + dst = os.path.join(dst_dir, Path(src_file).name) if not os.path.exists(dst): - shutil.copy(src, dst) + shutil.copy(src_file, dst) copy_ok = True - break return copy_ok From 3e8a414ce3cd7ed2e02c8842c689126ea5a762ef Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 26 Nov 2025 22:01:34 +0200 Subject: [PATCH 4/4] Fix formatting --- Mergin/project_settings_widget.py | 19 ++++++++++--------- Mergin/utils.py | 2 ++ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Mergin/project_settings_widget.py b/Mergin/project_settings_widget.py index 73197687..f4a30097 100644 --- a/Mergin/project_settings_widget.py +++ b/Mergin/project_settings_widget.py @@ -128,9 +128,11 @@ def __init__(self, parent=None): self.cmb_vertical_crs.setFilters(QgsCoordinateReferenceSystemProxyModel.FilterVertical) vcrs_def, ok = QgsProject.instance().readEntry("Mergin", "TargetVerticalCRS") - vertical_crs = QgsCoordinateReferenceSystem.fromWkt(vcrs_def) if ok else QgsCoordinateReferenceSystem.fromEpsgId(5773) #EGM96 geoid model + vertical_crs = ( + QgsCoordinateReferenceSystem.fromWkt(vcrs_def) if ok else QgsCoordinateReferenceSystem.fromEpsgId(5773) + ) # EGM96 geoid model self.cmb_vertical_crs.crsChanged.connect(self.geoid_model_path_change_state) - self.cmb_vertical_crs.setCrs(vertical_crs) + self.cmb_vertical_crs.setCrs(vertical_crs) self.cmb_vertical_crs.setOptionVisible(QgsProjectionSelectionWidget.CurrentCrs, True) self.cmb_vertical_crs.setDialogTitle("Target Vertical CRS") self.btn_get_geoid_file.clicked.connect(self.get_geoid_path) @@ -162,13 +164,12 @@ def geoid_model_path_change_state(self, newCRS): def get_geoid_path(self): # open the set location or user home - open_path = QFileInfo(self.edit_geoid_file.text()).absolutePath() if len(self.edit_geoid_file.text()) > 0 else os.path.expanduser("~") - abs_path = QFileDialog.getOpenFileName( - None, - "Select File", - open_path, - "Geoid Model Files (*.tif *.gtx)" + open_path = ( + QFileInfo(self.edit_geoid_file.text()).absolutePath() + if len(self.edit_geoid_file.text()) > 0 + else os.path.expanduser("~") ) + abs_path = QFileDialog.getOpenFileName(None, "Select File", open_path, "Geoid Model Files (*.tif *.gtx)") if len(abs_path[0]) > 0: self.edit_geoid_file.setText(abs_path[0]) @@ -356,7 +357,7 @@ def package_vcrs_file(self, vertical_crs): """ if len(self.edit_geoid_file.text()) == 0: return True - + project_proj_dir = os.path.join(mergin_project_local_path(), "proj") return copy_file_new(project_proj_dir, self.edit_geoid_file.text()) diff --git a/Mergin/utils.py b/Mergin/utils.py index d4d2f92c..23b46f50 100644 --- a/Mergin/utils.py +++ b/Mergin/utils.py @@ -1279,6 +1279,7 @@ def copy_datum_shift_grids(grids_dir): return missed_files + def copy_file_new(dst_dir, src_file): """ Copies file, which doesn't exist in destination directory, specified by absolute path. @@ -1293,6 +1294,7 @@ def copy_file_new(dst_dir, src_file): return copy_ok + def project_grids_directory(mp): """ Returns location of the "proj" directory inside MerginMaps project root directory