def __init__(self, layer, canvas, parent): super(MapLayerConfigWidget, self).__init__(layer, canvas, parent) self.setupUi(self) self.layer_source = LayerSource(layer) self.project = QgsProject.instance() set_available_actions(self.layerActionComboBox, self.layer_source) self.isGeometryLockedCheckBox.setEnabled(self.layer_source.can_lock_geometry) self.isGeometryLockedCheckBox.setChecked(self.layer_source.is_geometry_locked) self.photoNamingTable = PhotoNamingTableWidget() self.photoNamingTable.addLayerFields(self.layer_source) self.photoNamingTable.setLayerColumnHidden(True) # insert the table as a second row only for vector layers if Qgis.QGIS_VERSION_INT >= 31300 and layer.type() == QgsMapLayer.VectorLayer: self.layout().insertRow(1, self.tr('Photo Naming'), self.photoNamingTable) self.photoNamingTable.setEnabled(self.photoNamingTable.rowCount() > 0)
def toggle_menu_triggered(self, action): """ Toggles usae of layers :param action: the menu action that triggered this """ sync_action = SyncAction.NO_ACTION if action in (self.remove_hidden_action, self.remove_all_action): sync_action = SyncAction.REMOVE elif action in (self.add_all_offline_action, self.add_visible_offline_action): sync_action = SyncAction.OFFLINE # all layers if action in (self.remove_all_action, self.add_all_copy_action, self.add_all_offline_action): for i in range(self.layersTable.rowCount()): item = self.layersTable.item(i, 0) layer_source = item.data(Qt.UserRole) old_action = layer_source.action available_actions, _ = zip(*layer_source.available_actions) if sync_action in available_actions: layer_source.action = sync_action if layer_source.action != old_action: self.project.setDirty(True) layer_source.apply() # based on visibility elif action in (self.remove_hidden_action, self.add_visible_copy_action, self.add_visible_offline_action): visible = Qt.Unchecked if action == self.remove_hidden_action else Qt.Checked root = QgsProject.instance().layerTreeRoot() for layer in QgsProject.instance().mapLayers().values(): node = root.findLayer(layer.id()) if node and node.isVisible() == visible: layer_source = LayerSource(layer) old_action = layer_source.action available_actions, _ = zip(*layer_source.available_actions) if sync_action in available_actions: layer_source.action = sync_action if layer_source.action != old_action: self.project.setDirty(True) layer_source.apply() self.reloadProject()
def reloadProject(self): """ Load all layers from the map layer registry into the table. """ self.layersTable.setRowCount(0) self.layersTable.setSortingEnabled(False) for layer in self.project.mapLayers().values(): layer_source = LayerSource(layer) count = self.layersTable.rowCount() self.layersTable.insertRow(count) item = QTableWidgetItem(layer.name()) item.setData(Qt.UserRole, layer_source) item.setData(Qt.EditRole, layer.name()) self.layersTable.setItem(count, 0, item) cbx = QComboBox() for action, description in layer_source.available_actions: cbx.addItem(description) cbx.setItemData(cbx.count() - 1, action) if layer_source.action == action: cbx.setCurrentIndex(cbx.count() - 1) self.layersTable.setCellWidget(count, 1, cbx) self.layersTable.resizeColumnsToContents() self.layersTable.sortByColumn(0, Qt.AscendingOrder) self.layersTable.setSortingEnabled(True) # Load Map Themes for theme in self.project.mapThemeCollection().mapThemes(): self.mapThemeComboBox.addItem(theme) self.layerComboBox.setFilters(QgsMapLayerProxyModel.RasterLayer) self.__project_configuration = ProjectConfiguration(self.project) self.createBaseMapGroupBox.setChecked( self.__project_configuration.create_base_map) if self.__project_configuration.base_map_type == ProjectProperties.BaseMapType.SINGLE_LAYER: self.singleLayerRadioButton.setChecked(True) else: self.mapThemeRadioButton.setChecked(True) self.mapThemeComboBox.setCurrentIndex( self.mapThemeComboBox.findText( self.__project_configuration.base_map_theme)) layer = self.project.mapLayer( self.__project_configuration.base_map_layer) self.layerComboBox.setLayer(layer) self.mapUnitsPerPixel.setText( str(self.__project_configuration.base_map_mupp)) self.tileSize.setText( str(self.__project_configuration.base_map_tile_size)) self.onlyOfflineCopyFeaturesInAoi.setChecked( self.__project_configuration.offline_copy_only_aoi)
class MapLayerConfigWidget(QgsMapLayerConfigWidget, WidgetUi): def __init__(self, layer, canvas, parent): super(MapLayerConfigWidget, self).__init__(layer, canvas, parent) self.setupUi(self) self.layer_source = LayerSource(layer) self.project = QgsProject.instance() set_available_actions(self.layerActionComboBox, self.layer_source) self.isGeometryLockedCheckBox.setEnabled(self.layer_source.can_lock_geometry) self.isGeometryLockedCheckBox.setChecked(self.layer_source.is_geometry_locked) self.photoNamingTable = PhotoNamingTableWidget() self.photoNamingTable.addLayerFields(self.layer_source) self.photoNamingTable.setLayerColumnHidden(True) # insert the table as a second row only for vector layers if Qgis.QGIS_VERSION_INT >= 31300 and layer.type() == QgsMapLayer.VectorLayer: self.layout().insertRow(1, self.tr('Photo Naming'), self.photoNamingTable) self.photoNamingTable.setEnabled(self.photoNamingTable.rowCount() > 0) def apply(self): old_layer_action = self.layer_source.action old_is_geometry_locked = self.layer_source.is_geometry_locked self.layer_source.action = self.layerActionComboBox.itemData(self.layerActionComboBox.currentIndex()) self.layer_source.is_geometry_locked = self.isGeometryLockedCheckBox.isChecked() self.photoNamingTable.syncLayerSourceValues() # apply always the photo_namings (to store default values on first apply as well) if (self.layer_source.action != old_layer_action or self.layer_source.is_geometry_locked != old_is_geometry_locked or self.photoNamingTable.rowCount() > 0 ): self.layer_source.apply() self.project.setDirty(True)
def convert(self): """ Convert the project to a portable project. :param offline_editing: The offline editing instance :param export_folder: The folder to export to """ project = QgsProject.instance() original_project = project original_project_path = project.fileName() project_filename, _ = os.path.splitext( os.path.basename(original_project_path)) # Write a backup of the current project to a temporary file project_backup_folder = tempfile.mkdtemp() backup_project_path = os.path.join(project_backup_folder, project_filename + '.qgs') QgsProject.instance().write(backup_project_path) try: if not os.path.exists(self.export_folder): os.makedirs(self.export_folder) QApplication.setOverrideCursor(Qt.WaitCursor) self.__offline_layers = list() self.__layers = list(project.mapLayers().values()) original_layer_info = {} for layer in self.__layers: original_layer_info[layer.id()] = (layer.source(), layer.name()) # We store the pks of the original vector layers # and we check that the primary key fields names don't # have a comma in the name original_pk_fields_by_layer_name = {} for layer in self.__layers: if layer.type() == QgsMapLayer.VectorLayer: keys = [] for idx in layer.primaryKeyAttributes(): key = layer.fields()[idx].name() assert (',' not in key), 'Comma in field names not allowed' keys.append(key) original_pk_fields_by_layer_name[layer.name()] = ','.join( keys) self.total_progress_updated.emit(0, 1, self.trUtf8('Creating base map…')) # Create the base map before layers are removed if self.project_configuration.create_base_map: if 'processing' not in qgis.utils.plugins: QMessageBox.warning( None, self.tr('QFieldSync requires processing'), self. tr('Creating a basemap with QFieldSync requires the processing plugin to be enabled. Processing is not enabled on your system. Please go to Plugins > Manage and Install Plugins and enable processing.' )) return if self.project_configuration.base_map_type == ProjectProperties.BaseMapType.SINGLE_LAYER: self.createBaseMapLayer( None, self.project_configuration.base_map_layer, self.project_configuration.base_map_tile_size, self.project_configuration.base_map_mupp) else: self.createBaseMapLayer( self.project_configuration.base_map_theme, None, self.project_configuration.base_map_tile_size, self.project_configuration.base_map_mupp) # Loop through all layers and copy/remove/offline them pathResolver = QgsProject.instance().pathResolver() copied_files = list() for current_layer_index, layer in enumerate(self.__layers): self.total_progress_updated.emit( current_layer_index - len(self.__offline_layers), len(self.__layers), self.trUtf8('Copying layers…')) layer_source = LayerSource(layer) if not layer_source.is_supported: project.removeMapLayer(layer) continue if layer.dataProvider() is not None: md = QgsProviderRegistry.instance().providerMetadata( layer.dataProvider().name()) if md is not None: decoded = md.decodeUri(layer.source()) if "path" in decoded: path = pathResolver.writePath(decoded["path"]) if path.startswith("localized:"): # Layer stored in localized data path, skip continue if layer_source.action == SyncAction.OFFLINE: if self.project_configuration.offline_copy_only_aoi and not self.project_configuration.offline_copy_only_selected_features: layer.selectByRect(self.extent) elif self.project_configuration.offline_copy_only_aoi and self.project_configuration.offline_copy_only_selected_features: # This option is only possible via API QgsApplication.instance().messageLog().logMessage( self. tr('Both "Area of Interest" and "only selected features" options were enabled, tha latter takes precedence.' ), 'QFieldSync') self.__offline_layers.append(layer) # Store the primary key field name(s) as comma separated custom property if layer.type() == QgsMapLayer.VectorLayer: key_fields = ','.join([ layer.fields()[x].name() for x in layer.primaryKeyAttributes() ]) layer.setCustomProperty( 'QFieldSync/sourceDataPrimaryKeys', key_fields) elif layer_source.action == SyncAction.NO_ACTION: copied_files = layer_source.copy(self.export_folder, copied_files) elif layer_source.action == SyncAction.KEEP_EXISTENT: layer_source.copy(self.export_folder, copied_files, True) elif layer_source.action == SyncAction.REMOVE: project.removeMapLayer(layer) project_path = os.path.join(self.export_folder, project_filename + "_qfield.qgs") # save the original project path ProjectConfiguration( project).original_project_path = original_project_path # save the offline project twice so that the offline plugin can "know" that it's a relative path QgsProject.instance().write(project_path) # export the DCIM folder copy_images( os.path.join(os.path.dirname(original_project_path), "DCIM"), os.path.join(os.path.dirname(project_path), "DCIM")) try: # Run the offline plugin for gpkg gpkg_filename = "data.gpkg" if self.__offline_layers: offline_layer_ids = [l.id() for l in self.__offline_layers] only_selected = self.project_configuration.offline_copy_only_aoi or self.project_configuration.offline_copy_only_selected_features if not self.offline_editing.convertToOfflineProject( self.export_folder, gpkg_filename, offline_layer_ids, only_selected, self.offline_editing.GPKG): raise Exception( self. tr("Error trying to convert layers to offline layers" )) except AttributeError: # Run the offline plugin for spatialite spatialite_filename = "data.sqlite" if self.__offline_layers: offline_layer_ids = [l.id() for l in self.__offline_layers] only_selected = self.project_configuration.offline_copy_only_aoi or self.project_configuration.offline_copy_only_selected_features if not self.offline_editing.convertToOfflineProject( self.export_folder, spatialite_filename, offline_layer_ids, only_selected): raise Exception( self. tr("Error trying to convert layers to offline layers" )) # Disable project options that could create problems on a portable # project with offline layers if self.__offline_layers: QgsProject.instance().setEvaluateDefaultValues(False) QgsProject.instance().setAutoTransaction(False) # check if value relations point to offline layers and adjust if necessary for layer in project.mapLayers().values(): if layer.type() == QgsMapLayer.VectorLayer: # Before QGIS 3.14 the custom properties of a layer are not # kept into the new layer during the conversion to offline project # So we try to identify the new created layer by its name and # we set the custom properties again. if not layer.customProperty( 'QFieldSync/cloudPrimaryKeys'): original_layer_name = layer.name().rsplit(' ', 1)[0] stored_fields = original_pk_fields_by_layer_name.get( original_layer_name, None) if stored_fields: layer.setCustomProperty( 'QFieldSync/sourceDataPrimaryKeys', stored_fields) for field in layer.fields(): ews = field.editorWidgetSetup() if ews.type() == 'ValueRelation': widget_config = ews.config() online_layer_id = widget_config['Layer'] if project.mapLayer(online_layer_id): continue layer_id = None loose_layer_id = None for offline_layer in project.mapLayers( ).values(): if offline_layer.customProperty( 'remoteSource' ) == original_layer_info[online_layer_id][ 0]: # First try strict matching: the offline layer should have a "remoteSource" property layer_id = offline_layer.id() break elif offline_layer.name().startswith( original_layer_info[ online_layer_id][1] + ' '): # If that did not work, go with loose matching # The offline layer should start with the online layer name + a translated version of " (offline)" loose_layer_id = offline_layer.id() widget_config[ 'Layer'] = layer_id or loose_layer_id offline_ews = QgsEditorWidgetSetup( ews.type(), widget_config) layer.setEditorWidgetSetup( layer.fields().indexOf(field.name()), offline_ews) # Now we have a project state which can be saved as offline project QgsProject.instance().write(project_path) finally: # We need to let the app handle events before loading the next project or QGIS will crash with rasters QCoreApplication.processEvents() QgsProject.instance().clear() QCoreApplication.processEvents() QgsProject.instance().read(backup_project_path) QgsProject.instance().setFileName(original_project_path) QApplication.restoreOverrideCursor() self.offline_editing.layerProgressUpdated.disconnect( self.on_offline_editing_next_layer) self.offline_editing.progressModeSet.disconnect( self.on_offline_editing_max_changed) self.offline_editing.progressUpdated.disconnect( self.offline_editing_task_progress) self.total_progress_updated.emit(100, 100, self.tr('Finished'))
def reloadProject(self): """ Load all layers from the map layer registry into the table. """ self.unsupportedLayersList = list() self.photoNamingTable = PhotoNamingTableWidget() self.photoNamingTab.layout().addWidget(self.photoNamingTable) self.layersTable.setRowCount(0) self.layersTable.setSortingEnabled(False) for layer in self.project.mapLayers().values(): layer_source = LayerSource(layer) count = self.layersTable.rowCount() self.layersTable.insertRow(count) item = QTableWidgetItem(layer.name()) item.setData(Qt.UserRole, layer_source) item.setData(Qt.EditRole, layer.name()) self.layersTable.setItem(count, 0, item) cmb = QComboBox() set_available_actions(cmb, layer_source) cbx = QCheckBox() cbx.setEnabled(layer_source.can_lock_geometry) cbx.setChecked(layer_source.is_geometry_locked) # it's more UI friendly when the checkbox is centered, an ugly workaround to achieve it cbx_widget = QWidget() cbx_layout = QHBoxLayout() cbx_layout.setAlignment(Qt.AlignCenter) cbx_layout.setContentsMargins(0, 0, 0, 0) cbx_layout.addWidget(cbx) cbx_widget.setLayout(cbx_layout) # NOTE the margin is not updated when the table column is resized, so better rely on the code above # cbx.setStyleSheet("margin-left:50%; margin-right:50%;") self.layersTable.setCellWidget(count, 1, cbx_widget) self.layersTable.setCellWidget(count, 2, cmb) if not layer_source.is_supported: self.unsupportedLayersList.append(layer_source) self.layersTable.item(count,0).setFlags(Qt.NoItemFlags) self.layersTable.cellWidget(count,1).setEnabled(False) self.layersTable.cellWidget(count,2).setEnabled(False) cmb.setCurrentIndex(cmb.findData(SyncAction.REMOVE)) # make sure layer_source is the same instance everywhere self.photoNamingTable.addLayerFields(layer_source) self.layersTable.resizeColumnsToContents() self.layersTable.sortByColumn(0, Qt.AscendingOrder) self.layersTable.setSortingEnabled(True) # Remove the tab when not yet suported in QGIS if Qgis.QGIS_VERSION_INT < 31300: self.tabWidget.removeTab(self.tabWidget.count() - 1) # Load Map Themes for theme in self.project.mapThemeCollection().mapThemes(): self.mapThemeComboBox.addItem(theme) self.layerComboBox.setFilters(QgsMapLayerProxyModel.RasterLayer) self.__project_configuration = ProjectConfiguration(self.project) self.createBaseMapGroupBox.setChecked(self.__project_configuration.create_base_map) if self.__project_configuration.base_map_type == ProjectProperties.BaseMapType.SINGLE_LAYER: self.singleLayerRadioButton.setChecked(True) else: self.mapThemeRadioButton.setChecked(True) self.mapThemeComboBox.setCurrentIndex( self.mapThemeComboBox.findText(self.__project_configuration.base_map_theme)) layer = QgsProject.instance().mapLayer(self.__project_configuration.base_map_layer) self.layerComboBox.setLayer(layer) self.mapUnitsPerPixel.setText(str(self.__project_configuration.base_map_mupp)) self.tileSize.setText(str(self.__project_configuration.base_map_tile_size)) self.onlyOfflineCopyFeaturesInAoi.setChecked(self.__project_configuration.offline_copy_only_aoi) if self.unsupportedLayersList: self.unsupportedLayersLabel.setVisible(True) unsupported_layers_text = '<b>{}: </b>'.format(self.tr('Warning')) unsupported_layers_text += self.tr("There are unsupported layers in your project which will not be available in QField.") unsupported_layers_text += self.tr(" If needed, you can create a Base Map to include those layers in your packaged project.") self.unsupportedLayersLabel.setText(unsupported_layers_text)
def reloadProject(self): """ Load all layers from the map layer registry into the table. """ self.unsupportedLayersList = list() self.layersTable.setRowCount(0) self.layersTable.setSortingEnabled(False) for layer in list(self.project.mapLayers().values()): layer_source = LayerSource(layer) if not layer_source.is_supported: self.unsupportedLayersList.append(layer_source) count = self.layersTable.rowCount() self.layersTable.insertRow(count) item = QTableWidgetItem(layer.name()) item.setData(Qt.UserRole, layer_source) item.setData(Qt.EditRole, layer.name()) self.layersTable.setItem(count, 0, item) cbx = QComboBox() for action, description in layer_source.available_actions: cbx.addItem(description) cbx.setItemData(cbx.count() - 1, action) if layer_source.action == action: cbx.setCurrentIndex(cbx.count() - 1) self.layersTable.setCellWidget(count, 1, cbx) self.layersTable.resizeColumnsToContents() self.layersTable.sortByColumn(0, Qt.AscendingOrder) self.layersTable.setSortingEnabled(True) # Load Map Themes for theme in self.project.mapThemeCollection().mapThemes(): self.mapThemeComboBox.addItem(theme) self.layerComboBox.setFilters(QgsMapLayerProxyModel.RasterLayer) self.__project_configuration = ProjectConfiguration(self.project) self.createBaseMapGroupBox.setChecked( self.__project_configuration.create_base_map) if self.__project_configuration.base_map_type == ProjectProperties.BaseMapType.SINGLE_LAYER: self.singleLayerRadioButton.setChecked(True) else: self.mapThemeRadioButton.setChecked(True) self.mapThemeComboBox.setCurrentIndex( self.mapThemeComboBox.findText( self.__project_configuration.base_map_theme)) layer = QgsProject.instance().mapLayer( self.__project_configuration.base_map_layer) self.layerComboBox.setLayer(layer) self.mapUnitsPerPixel.setText( str(self.__project_configuration.base_map_mupp)) self.tileSize.setText( str(self.__project_configuration.base_map_tile_size)) self.onlyOfflineCopyFeaturesInAoi.setChecked( self.__project_configuration.offline_copy_only_aoi) if self.unsupportedLayersList: self.unsupportedLayers.setVisible(True) unsuppoerted_layers_text = '<b>{}</b><br>'.format( self.tr('Warning')) unsuppoerted_layers_text += self.tr( "There are unsupported layers in your project. They will not be available on QField." ) unsuppoerted_layers_text += '<ul>' for layer in self.unsupportedLayersList: unsuppoerted_layers_text += '<li>' + '<b>' + layer.name + ':</b> ' + layer.warning unsuppoerted_layers_text += '<ul>' self.unsupportedLayers.setText(unsuppoerted_layers_text)
def convert(self): """ Convert the project to a portable project. :param offline_editing: The offline editing instance :param export_folder: The folder to export to """ project = QgsProject.instance() original_project_path = project.fileName() project_filename, _ = os.path.splitext(os.path.basename(original_project_path)) # Write a backup of the current project to a temporary file project_backup_folder = tempfile.mkdtemp() backup_project_path = os.path.join(project_backup_folder, project_filename + '.qgs') QgsProject.instance().write(backup_project_path) try: if not os.path.exists(self.export_folder): os.makedirs(self.export_folder) QApplication.setOverrideCursor(Qt.WaitCursor) self.__offline_layers = list() self.__layers = list(project.mapLayers().values()) self.total_progress_updated.emit(0, 1, self.tr('Creating base map')) # Create the base map before layers are removed if self.project_configuration.create_base_map: if 'processing' not in qgis.utils.plugins: QMessageBox.warning(None, self.tr('QFieldSync requires processing'), self.tr('Creating a basemap with QFieldSync requires the processing plugin to be enabled. Processing is not enabled on your system. Please go to Plugins > Manage and Install Plugins and enable processing.')) return if self.project_configuration.base_map_type == ProjectProperties.BaseMapType.SINGLE_LAYER: self.createBaseMapLayer(None, self.project_configuration.base_map_layer, self.project_configuration.base_map_tile_size, self.project_configuration.base_map_mupp) else: self.createBaseMapLayer(self.project_configuration.base_map_theme, None, self.project_configuration.base_map_tile_size, self.project_configuration.base_map_mupp) # Loop through all layers and copy/remove/offline them copied_files = list() for current_layer_index, layer in enumerate(self.__layers): self.total_progress_updated.emit(current_layer_index - len(self.__offline_layers), len(self.__layers), self.tr('Copying layers')) layer_source = LayerSource(layer) if layer_source.action == SyncAction.OFFLINE: if self.project_configuration.offline_copy_only_aoi: layer.selectByRect(self.extent) self.__offline_layers.append(layer) elif layer_source.action == SyncAction.NO_ACTION: copied_files = layer_source.copy(self.export_folder, copied_files) elif layer_source.action == SyncAction.KEEP_EXISTENT: layer_source.copy(self.export_folder, copied_files, True) elif layer_source.action == SyncAction.REMOVE: project.removeMapLayer(layer) project_path = os.path.join(self.export_folder, project_filename + "_qfield.qgs") # save the original project path ProjectConfiguration(project).original_project_path = original_project_path # save the offline project twice so that the offline plugin can "know" that it's a relative path QgsProject.instance().write(project_path) try: # Run the offline plugin for gpkg gpkg_filename = "data.gpkg" if self.__offline_layers: offline_layer_ids = [l.id() for l in self.__offline_layers] if not self.offline_editing.convertToOfflineProject(self.export_folder, gpkg_filename, offline_layer_ids, self.project_configuration.offline_copy_only_aoi, self.offline_editing.GPKG): raise Exception(self.tr("Error trying to convert layers to offline layers")) except AttributeError: # Run the offline plugin for spatialite spatialite_filename = "data.sqlite" if self.__offline_layers: offline_layer_ids = [l.id() for l in self.__offline_layers] if not self.offline_editing.convertToOfflineProject(self.export_folder, spatialite_filename, offline_layer_ids, self.project_configuration.offline_copy_only_aoi): raise Exception(self.tr("Error trying to convert layers to offline layers")) # Disable project options that could create problems on a portable # project with offline layers if self.__offline_layers: QgsProject.instance().setEvaluateDefaultValues(False) QgsProject.instance().setAutoTransaction(False) # Now we have a project state which can be saved as offline project QgsProject.instance().write(project_path) finally: # We need to let the app handle events before loading the next project or QGIS will crash with rasters QCoreApplication.processEvents() QgsProject.instance().clear() QCoreApplication.processEvents() QgsProject.instance().read(backup_project_path) QgsProject.instance().setFileName(original_project_path) QApplication.restoreOverrideCursor() self.total_progress_updated.emit(100, 100, self.tr('Finished'))
def convert(self): """ Convert the project to a portable project. :param offline_editing: The offline editing instance :param export_folder: The folder to export to """ project = QgsProject.instance() original_project = project original_project_path = project.fileName() project_filename, _ = os.path.splitext( os.path.basename(original_project_path)) # Write a backup of the current project to a temporary file project_backup_folder = tempfile.mkdtemp() backup_project_path = os.path.join(project_backup_folder, project_filename + '.qgs') QgsProject.instance().write(backup_project_path) try: if not os.path.exists(self.export_folder): os.makedirs(self.export_folder) QApplication.setOverrideCursor(Qt.WaitCursor) self.__offline_layers = list() self.__layers = list(project.mapLayers().values()) original_layer_info = {} for layer in self.__layers: original_layer_info[layer.id()] = (layer.source(), layer.name()) self.total_progress_updated.emit(0, 1, self.trUtf8('Creating base map…')) # Create the base map before layers are removed if self.project_configuration.create_base_map: if 'processing' not in qgis.utils.plugins: QMessageBox.warning( None, self.tr('QFieldSync requires processing'), self. tr('Creating a basemap with QFieldSync requires the processing plugin to be enabled. Processing is not enabled on your system. Please go to Plugins > Manage and Install Plugins and enable processing.' )) return if self.project_configuration.base_map_type == ProjectProperties.BaseMapType.SINGLE_LAYER: self.createBaseMapLayer( None, self.project_configuration.base_map_layer, self.project_configuration.base_map_tile_size, self.project_configuration.base_map_mupp) else: self.createBaseMapLayer( self.project_configuration.base_map_theme, None, self.project_configuration.base_map_tile_size, self.project_configuration.base_map_mupp) # Loop through all layers and copy/remove/offline them copied_files = list() for current_layer_index, layer in enumerate(self.__layers): self.total_progress_updated.emit( current_layer_index - len(self.__offline_layers), len(self.__layers), self.trUtf8('Copying layers…')) layer_source = LayerSource(layer) if layer_source.action == SyncAction.OFFLINE: if self.project_configuration.offline_copy_only_aoi: layer.selectByRect(self.extent) self.__offline_layers.append(layer) elif layer_source.action == SyncAction.NO_ACTION: copied_files = layer_source.copy(self.export_folder, copied_files) elif layer_source.action == SyncAction.KEEP_EXISTENT: layer_source.copy(self.export_folder, copied_files, True) elif layer_source.action == SyncAction.REMOVE: project.removeMapLayer(layer) project_path = os.path.join(self.export_folder, project_filename + "_qfield.qgs") # save the original project path ProjectConfiguration( project).original_project_path = original_project_path # save the offline project twice so that the offline plugin can "know" that it's a relative path QgsProject.instance().write(project_path) # export the DCIM folder copy_images( os.path.join(os.path.dirname(original_project_path), "DCIM"), os.path.join(os.path.dirname(project_path), "DCIM")) try: # Run the offline plugin for gpkg gpkg_filename = "data.gpkg" if self.__offline_layers: offline_layer_ids = [l.id() for l in self.__offline_layers] if not self.offline_editing.convertToOfflineProject( self.export_folder, gpkg_filename, offline_layer_ids, self.project_configuration.offline_copy_only_aoi, self.offline_editing.GPKG): raise Exception( self. tr("Error trying to convert layers to offline layers" )) except AttributeError: # Run the offline plugin for spatialite spatialite_filename = "data.sqlite" if self.__offline_layers: offline_layer_ids = [l.id() for l in self.__offline_layers] if not self.offline_editing.convertToOfflineProject( self.export_folder, spatialite_filename, offline_layer_ids, self.project_configuration.offline_copy_only_aoi): raise Exception( self. tr("Error trying to convert layers to offline layers" )) # Disable project options that could create problems on a portable # project with offline layers if self.__offline_layers: QgsProject.instance().setEvaluateDefaultValues(False) QgsProject.instance().setAutoTransaction(False) # check if value relations point to offline layers and adjust if necessary for layer in project.mapLayers().values(): if layer.type() == QgsMapLayer.VectorLayer: for field in layer.fields(): ews = field.editorWidgetSetup() if ews.type() == 'ValueRelation': widget_config = ews.config() online_layer_id = widget_config['Layer'] if project.mapLayer(online_layer_id): continue layer_id = None loose_layer_id = None for offline_layer in project.mapLayers( ).values(): if offline_layer.customProperty( 'remoteSource' ) == original_layer_info[online_layer_id][ 0]: # First try strict matching: the offline layer should have a "remoteSource" property layer_id = offline_layer.id() break elif offline_layer.name().startswith( original_layer_info[ online_layer_id][1] + ' '): # If that did not work, go with loose matching # The offline layer should start with the online layer name + a translated version of " (offline)" loose_layer_id = offline_layer.id() widget_config[ 'Layer'] = layer_id or loose_layer_id offline_ews = QgsEditorWidgetSetup( ews.type(), widget_config) layer.setEditorWidgetSetup( layer.fields().indexOf(field.name()), offline_ews) # Now we have a project state which can be saved as offline project QgsProject.instance().write(project_path) finally: # We need to let the app handle events before loading the next project or QGIS will crash with rasters QCoreApplication.processEvents() QgsProject.instance().clear() QCoreApplication.processEvents() QgsProject.instance().read(backup_project_path) QgsProject.instance().setFileName(original_project_path) QApplication.restoreOverrideCursor() self.total_progress_updated.emit(100, 100, self.tr('Finished'))
def convert(self): """ Convert the project to a portable project. :param offline_editing: The offline editing instance :param export_folder: The folder to export to """ project = QgsProject.instance() original_project_path = project.fileName() project_filename, _ = os.path.splitext( os.path.basename(original_project_path)) # Write a backup of the current project to a temporary file project_backup_folder = tempfile.mkdtemp() backup_project_path = os.path.join(project_backup_folder, project_filename + '.qgs') QgsProject.instance().write(backup_project_path) try: if not os.path.exists(self.export_folder): os.makedirs(self.export_folder) QApplication.setOverrideCursor(Qt.WaitCursor) self.__offline_layers = list() self.__layers = list(project.mapLayers().values()) self.total_progress_updated.emit(0, 1, self.tr('Creating base map')) # Create the base map before layers are removed if self.project_configuration.create_base_map: if 'processing' not in qgis.utils.plugins: QMessageBox.warning( None, self.tr('QFieldSync requires processing'), self. tr('Creating a basemap with QFieldSync requires the processing plugin to be enabled. Processing is not enabled on your system. Please go to Plugins > Manage and Install Plugins and enable processing.' )) return if self.project_configuration.base_map_type == ProjectProperties.BaseMapType.SINGLE_LAYER: self.createBaseMapLayer( None, self.project_configuration.base_map_layer, self.project_configuration.base_map_tile_size, self.project_configuration.base_map_mupp) else: self.createBaseMapLayer( self.project_configuration.base_map_theme, None, self.project_configuration.base_map_tile_size, self.project_configuration.base_map_mupp) # Loop through all layers and copy/remove/offline them copied_files = list() for current_layer_index, layer in enumerate(self.__layers): self.total_progress_updated.emit( current_layer_index - len(self.__offline_layers), len(self.__layers), self.tr('Copying layers')) layer_source = LayerSource(layer) if layer_source.action == SyncAction.OFFLINE: if self.project_configuration.offline_copy_only_aoi: layer.selectByRect(self.extent) self.__offline_layers.append(layer) elif layer_source.action == SyncAction.NO_ACTION: copied_files = layer_source.copy(self.export_folder, copied_files) elif layer_source.action == SyncAction.KEEP_EXISTENT: layer_source.copy(self.export_folder, copied_files, True) elif layer_source.action == SyncAction.REMOVE: project.removeMapLayer(layer) project_path = os.path.join(self.export_folder, project_filename + "_qfield.qgs") # save the original project path ProjectConfiguration( project).original_project_path = original_project_path # save the offline project twice so that the offline plugin can "know" that it's a relative path QgsProject.instance().write(project_path) try: # Run the offline plugin for gpkg gpkg_filename = "data.gpkg" if self.__offline_layers: offline_layer_ids = [l.id() for l in self.__offline_layers] if not self.offline_editing.convertToOfflineProject( self.export_folder, gpkg_filename, offline_layer_ids, self.project_configuration.offline_copy_only_aoi, self.offline_editing.GPKG): raise Exception( self. tr("Error trying to convert layers to offline layers" )) except AttributeError: # Run the offline plugin for spatialite spatialite_filename = "data.sqlite" if self.__offline_layers: offline_layer_ids = [l.id() for l in self.__offline_layers] if not self.offline_editing.convertToOfflineProject( self.export_folder, spatialite_filename, offline_layer_ids, self.project_configuration.offline_copy_only_aoi): raise Exception( self. tr("Error trying to convert layers to offline layers" )) # Now we have a project state which can be saved as offline project QgsProject.instance().write(project_path) finally: # We need to let the app handle events before loading the next project or QGIS will crash with rasters QCoreApplication.processEvents() QgsProject.instance().clear() QCoreApplication.processEvents() QgsProject.instance().read(backup_project_path) QgsProject.instance().setFileName(original_project_path) QApplication.restoreOverrideCursor() self.total_progress_updated.emit(100, 100, self.tr('Finished'))
def convert_to_offline(self, db, surveyor_expression_dict, export_dir): sys.path.append(PLUGINS_DIR) from qfieldsync.core.layer import LayerSource, SyncAction from qfieldsync.core.offline_converter import OfflineConverter from qfieldsync.core.project import ProjectConfiguration project = QgsProject.instance() extent = QgsRectangle() offline_editing = QgsOfflineEditing() # Configure project project_configuration = ProjectConfiguration(project) project_configuration.create_base_map = False project_configuration.offline_copy_only_aoi = False project_configuration.use_layer_selection = True # Layer config layer_sync_action = LayerConfig.get_field_data_capture_layer_config( db.names) total_projects = len(surveyor_expression_dict) current_progress = 0 for surveyor, layer_config in surveyor_expression_dict.items(): export_folder = os.path.join(export_dir, surveyor) # Get layers (cannot be done out of this for loop because the project is closed and layers are deleted) layers = { layer_name: None for layer_name, _ in layer_sync_action.items() } self.app.core.get_layers(db, layers, True) if not layers: return False, QCoreApplication.translate( "FieldDataCapture", "At least one layer could not be found.") # Configure layers for layer_name, layer in layers.items(): layer_source = LayerSource(layer) layer_source.action = layer_sync_action[layer_name] if layer_name in layer_config: layer_source.select_expression = layer_config[layer_name] layer_source.apply() offline_converter = OfflineConverter(project, export_folder, extent, offline_editing) offline_converter.convert() offline_editing.layerProgressUpdated.disconnect( offline_converter.on_offline_editing_next_layer) offline_editing.progressModeSet.disconnect( offline_converter.on_offline_editing_max_changed) offline_editing.progressUpdated.disconnect( offline_converter.offline_editing_task_progress) current_progress += 1 self.total_progress_updated.emit( int(100 * current_progress / total_projects)) return True, QCoreApplication.translate( "FieldDataCapture", "{count} offline projects have been successfully created in <a href='file:///{normalized_path}'>{path}</a>!" ).format(count=total_projects, normalized_path=normalize_local_url(export_dir), path=export_dir)
def supportsLayer(self, layer): return LayerSource(layer).is_supported