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)
예제 #2
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()
예제 #3
0
    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)
    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()
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)
예제 #6
0
    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'))
예제 #7
0
    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)
예제 #8
0
    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)
예제 #9
0
    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'))
예제 #10
0
    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'))
예제 #11
0
    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'))
예제 #12
0
    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