""" /*************************************************************************** Name : DB Manager Description : Database manager plugin for QGIS Date : Oct 13, 2011 copyright : (C) 2011 by Giuseppe Sucameli email : brush.tyler@gmail.com The content of this file is based on - PG_Manager by Martin Dobias (GPLv2 license) ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ """ from qgis.PyQt import uic from qgis.PyQt.QtCore import Qt, QFileInfo from qgis.PyQt.QtWidgets import QDialog, QFileDialog, QMessageBox from qgis.core import ( QgsDataSourceUri, QgsVectorDataProvider, QgsVectorLayer, QgsMapLayerType, QgsProviderRegistry, QgsCoordinateReferenceSystem, QgsVectorLayerExporter, QgsProject, QgsSettings, ) from qgis.gui import QgsMessageViewer from qgis.utils import OverrideCursor, iface from .gui_utils import GuiUtils Ui_Dialog, _ = uic.loadUiType(GuiUtils.get_ui_file_path("DlgImportVector.ui")) class DlgImportVector(QDialog, Ui_Dialog): HAS_INPUT_MODE, ASK_FOR_INPUT_MODE = list(range(2)) def __init__(self, inLayer, outDb, outUri, parent=None): QDialog.__init__(self, parent) self.inLayer = inLayer self.db = outDb self.outUri = outUri self.setupUi(self) supportCom = self.db.supportsComment() if not supportCom: self.chkCom.setVisible(False) self.editCom.setVisible(False) self.default_pk = "id" self.default_geom = "geom" self.mode = ( self.ASK_FOR_INPUT_MODE if self.inLayer is None else self.HAS_INPUT_MODE ) # used to delete the inlayer whether created inside this dialog self.inLayerMustBeDestroyed = False self.populateSchemas() self.populateTables() self.populateLayers() self.populateEncodings() # updates of UI self.setupWorkingMode(self.mode) self.cboSchema.currentIndexChanged.connect(self.populateTables) self.widgetSourceSrid.setCrs(QgsProject.instance().crs()) self.widgetTargetSrid.setCrs(QgsProject.instance().crs()) self.updateInputLayer() def setupWorkingMode(self, mode): """hide the widget to select a layer/file if the input layer is already set""" self.wdgInput.setVisible(mode == self.ASK_FOR_INPUT_MODE) self.resize(450, 350) self.cboTable.setEditText(self.outUri.table()) if mode == self.ASK_FOR_INPUT_MODE: self.btnChooseInputFile.clicked.connect(self.chooseInputFile) self.cboInputLayer.currentTextChanged.connect(self.updateInputLayer) self.editPrimaryKey.setText(self.default_pk) self.editGeomColumn.setText(self.default_geom) self.chkLowercaseFieldNames.setEnabled( self.db.hasLowercaseFieldNamesOption() ) if not self.chkLowercaseFieldNames.isEnabled(): self.chkLowercaseFieldNames.setChecked(False) else: # set default values self.checkSupports() self.updateInputLayer() def checkSupports(self): """update options available for the current input layer""" allowSpatial = self.db.connector.hasSpatialSupport() hasGeomType = self.inLayer and self.inLayer.isSpatial() isShapefile = ( self.inLayer and self.inLayer.providerType() == "ogr" and self.inLayer.storageType() == "ESRI Shapefile" ) self.chkGeomColumn.setEnabled(allowSpatial and hasGeomType) if not self.chkGeomColumn.isEnabled(): self.chkGeomColumn.setChecked(False) self.chkSourceSrid.setEnabled(allowSpatial and hasGeomType) if not self.chkSourceSrid.isEnabled(): self.chkSourceSrid.setChecked(False) self.chkTargetSrid.setEnabled(allowSpatial and hasGeomType) if not self.chkTargetSrid.isEnabled(): self.chkTargetSrid.setChecked(False) self.chkSinglePart.setEnabled(allowSpatial and hasGeomType and isShapefile) if not self.chkSinglePart.isEnabled(): self.chkSinglePart.setChecked(False) self.chkSpatialIndex.setEnabled(allowSpatial and hasGeomType) if not self.chkSpatialIndex.isEnabled(): self.chkSpatialIndex.setChecked(False) self.chkLowercaseFieldNames.setEnabled(self.db.hasLowercaseFieldNamesOption()) if not self.chkLowercaseFieldNames.isEnabled(): self.chkLowercaseFieldNames.setChecked(False) def populateLayers(self): self.cboInputLayer.clear() for nodeLayer in QgsProject.instance().layerTreeRoot().findLayers(): layer = nodeLayer.layer() # TODO: add import raster support! if layer is not None and layer.type() == QgsMapLayerType.VectorLayer: self.cboInputLayer.addItem(layer.name(), layer.id()) # set the current index of the combo box to the active layer in the layer tree (if found in combo box) if iface is not None and iface.activeLayer(): index = self.cboInputLayer.findData(iface.activeLayer().id()) if index != -1: self.cboInputLayer.setCurrentIndex(index) def deleteInputLayer(self): """unset the input layer, then destroy it but only if it was created from this dialog""" if self.mode == self.ASK_FOR_INPUT_MODE and self.inLayer: if self.inLayerMustBeDestroyed: self.inLayer.deleteLater() self.inLayer = None self.inLayerMustBeDestroyed = False return True return False def chooseInputFile(self): vectorFormats = QgsProviderRegistry.instance().fileVectorFilters() # get last used dir and format settings = QgsSettings() lastDir = settings.value("/db_manager/lastUsedDir", "") lastVectorFormat = settings.value("/UI/lastVectorFileFilter", "") # ask for a filename filename, lastVectorFormat = QFileDialog.getOpenFileName( self, self.tr("Choose the file to import"), lastDir, vectorFormats, lastVectorFormat, ) if filename == "": return # store the last used dir and format settings.setValue("/db_manager/lastUsedDir", QFileInfo(filename).filePath()) settings.setValue("/UI/lastVectorFileFilter", lastVectorFormat) self.cboInputLayer.setCurrentIndex(-1) self.cboInputLayer.setEditText(filename) def reloadInputLayer(self): """Creates the input layer and update available options""" if self.mode != self.ASK_FOR_INPUT_MODE: return True self.deleteInputLayer() index = self.cboInputLayer.currentIndex() if index < 0: filename = self.cboInputLayer.currentText() if filename == "": return False layerName = QFileInfo(filename).completeBaseName() layer = QgsVectorLayer(filename, layerName, "ogr") if not layer.isValid() or layer.type() != QgsMapLayerType.VectorLayer: layer.deleteLater() return False self.inLayer = layer self.inLayerMustBeDestroyed = True else: layerId = self.cboInputLayer.itemData(index) self.inLayer = QgsProject.instance().mapLayer(layerId) self.inLayerMustBeDestroyed = False self.checkSupports() return True def updateInputLayer(self): if not self.reloadInputLayer() or not self.inLayer: return False # update the output table name, pk and geom column self.cboTable.setEditText(self.inLayer.name()) srcUri = QgsDataSourceUri(self.inLayer.source()) pk = srcUri.keyColumn() if srcUri.keyColumn() else self.default_pk self.editPrimaryKey.setText(pk) geom = srcUri.geometryColumn() if srcUri.geometryColumn() else self.default_geom self.editGeomColumn.setText(geom) srcCrs = self.inLayer.crs() if not srcCrs.isValid(): srcCrs = QgsCoordinateReferenceSystem("EPSG:4326") self.widgetSourceSrid.setCrs(srcCrs) self.widgetTargetSrid.setCrs(srcCrs) return True def populateSchemas(self): if not self.db: return self.cboSchema.clear() schemas = self.db.schemas() if schemas is None: self.hideSchemas() return index = -1 for schema in schemas: self.cboSchema.addItem(schema.name) if schema.name == self.outUri.schema(): index = self.cboSchema.count() - 1 self.cboSchema.setCurrentIndex(index) def hideSchemas(self): self.cboSchema.setEnabled(False) def populateTables(self): if not self.db: return currentText = self.cboTable.currentText() schemas = self.db.schemas() if schemas is not None: schema_name = self.cboSchema.currentText() matching_schemas = [x for x in schemas if x.name == schema_name] tables = matching_schemas[0].tables() if len(matching_schemas) > 0 else [] else: tables = self.db.tables() self.cboTable.clear() for table in tables: self.cboTable.addItem(table.name) self.cboTable.setEditText(currentText) def populateEncodings(self): # populate the combo with supported encodings self.cboEncoding.addItems(QgsVectorDataProvider.availableEncodings()) self.cboEncoding.insertItem(0, self.tr("Automatic"), "") self.cboEncoding.setCurrentIndex(0) def accept(self): if self.mode == self.ASK_FOR_INPUT_MODE: # create the input layer (if not already done) and # update available options self.reloadInputLayer() # sanity checks if self.inLayer is None: QMessageBox.critical( self, self.tr("Import to Database"), self.tr("Input layer missing or not valid."), ) return if self.cboTable.currentText() == "": QMessageBox.critical( self, self.tr("Import to Database"), self.tr("Output table name is required."), ) return if self.chkSourceSrid.isEnabled() and self.chkSourceSrid.isChecked(): if not self.widgetSourceSrid.crs().isValid(): QMessageBox.critical( self, self.tr("Import to Database"), self.tr("Invalid source srid: must be a valid crs."), ) return if self.chkTargetSrid.isEnabled() and self.chkTargetSrid.isChecked(): if not self.widgetTargetSrid.crs().isValid(): QMessageBox.critical( self, self.tr("Import to Database"), self.tr("Invalid target srid: must be a valid crs."), ) return with OverrideCursor(Qt.CursorShape.WaitCursor): # store current input layer crs and encoding, so I can restore it prevInCrs = self.inLayer.crs() prevInEncoding = self.inLayer.dataProvider().encoding() try: schema = ( self.outUri.schema() if not self.cboSchema.isEnabled() else self.cboSchema.currentText() ) table = self.cboTable.currentText() # get pk and geom field names from the source layer or use the # ones defined by the user srcUri = QgsDataSourceUri(self.inLayer.source()) pk = ( srcUri.keyColumn() if not self.chkPrimaryKey.isChecked() else self.editPrimaryKey.text() ) if not pk: pk = self.default_pk if self.inLayer.isSpatial() and self.chkGeomColumn.isEnabled(): geom = ( srcUri.geometryColumn() if not self.chkGeomColumn.isChecked() else self.editGeomColumn.text() ) if not geom: geom = self.default_geom else: geom = None options = {} if ( self.chkLowercaseFieldNames.isEnabled() and self.chkLowercaseFieldNames.isChecked() ): pk = pk.lower() if geom: geom = geom.lower() options["lowercaseFieldNames"] = True # get output params, update output URI self.outUri.setDataSource(schema, table, geom, "", pk) typeName = self.db.dbplugin().typeName() providerName = self.db.dbplugin().providerName() if typeName == "gpkg": uri = self.outUri.database() options["update"] = True options["driverName"] = "GPKG" options["layerName"] = table else: uri = self.outUri.uri(False) if self.chkDropTable.isChecked(): options["overwrite"] = True if self.chkSinglePart.isEnabled() and self.chkSinglePart.isChecked(): options["forceSinglePartGeometryType"] = True outCrs = QgsCoordinateReferenceSystem() if self.chkTargetSrid.isEnabled() and self.chkTargetSrid.isChecked(): outCrs = self.widgetTargetSrid.crs() # update input layer crs and encoding if self.chkSourceSrid.isEnabled() and self.chkSourceSrid.isChecked(): inCrs = self.widgetSourceSrid.crs() self.inLayer.setCrs(inCrs) if ( self.chkEncoding.isEnabled() and self.chkEncoding.isChecked() and self.cboEncoding.currentData() is None ): enc = self.cboEncoding.currentText() self.inLayer.setProviderEncoding(enc) onlySelected = self.chkSelectedFeatures.isChecked() # do the import! ret, errMsg = QgsVectorLayerExporter.exportLayer( self.inLayer, uri, providerName, outCrs, onlySelected, options ) except Exception as e: ret = -1 errMsg = str(e) finally: # restore input layer crs and encoding self.inLayer.setCrs(prevInCrs) self.inLayer.setProviderEncoding(prevInEncoding) if ret != 0: output = QgsMessageViewer() output.setTitle(self.tr("Import to Database")) output.setMessageAsPlainText(self.tr("Error {0}\n{1}").format(ret, errMsg)) output.showMessage() return # create spatial index if self.chkSpatialIndex.isEnabled() and self.chkSpatialIndex.isChecked(): self.db.connector.createSpatialIndex((schema, table), geom) # add comment on table supportCom = self.db.supportsComment() if self.chkCom.isEnabled() and self.chkCom.isChecked() and supportCom: # using connector executing COMMENT ON TABLE query (with editCome.text() value) com = self.editCom.toPlainText() self.db.connector.commentTable(schema, table, com) self.db.connection().reconnect() self.db.refresh() QMessageBox.information( self, self.tr("Import to Database"), self.tr("Import was successful.") ) return QDialog.accept(self) def closeEvent(self, event): # destroy the input layer instance but only if it was created # from this dialog! self.deleteInputLayer() QDialog.closeEvent(self, event)