123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660 |
- """
- /***************************************************************************
- Name : DB Manager
- Description : Database manager plugin for QGIS
- Date : May 23, 2011
- copyright : (C) 2011 by Giuseppe Sucameli
- email : brush.tyler@gmail.com
- ***************************************************************************/
- /***************************************************************************
- * *
- * 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 functools import partial
- from qgis.PyQt.QtCore import Qt, QObject, qDebug, QByteArray, QMimeData, QDataStream, QIODevice, QFileInfo, QAbstractItemModel, QModelIndex, pyqtSignal
- from qgis.PyQt.QtWidgets import QApplication, QMessageBox
- from qgis.PyQt.QtGui import QIcon
- from .db_plugins import supportedDbTypes, createDbPlugin
- from .db_plugins.plugin import BaseError, Table, Database
- from .dlg_db_error import DlgDbError
- from qgis.core import (
- QgsApplication,
- QgsDataSourceUri,
- QgsVectorLayer,
- QgsRasterLayer,
- QgsMimeDataUtils,
- QgsProviderConnectionException,
- QgsProviderRegistry,
- QgsAbstractDatabaseProviderConnection,
- QgsMessageLog,
- )
- from qgis.utils import OverrideCursor
- from . import resources_rc # NOQA
- try:
- from qgis.core import QgsVectorLayerExporter # NOQA
- isImportVectorAvail = True
- except:
- isImportVectorAvail = False
- class TreeItem(QObject):
- deleted = pyqtSignal()
- changed = pyqtSignal()
- def __init__(self, data, parent=None):
- QObject.__init__(self, parent)
- self.populated = False
- self.itemData = data
- self.childItems = []
- if parent:
- parent.appendChild(self)
- def childRemoved(self):
- self.itemChanged()
- def itemChanged(self):
- self.changed.emit()
- def itemDeleted(self):
- self.deleted.emit()
- def populate(self):
- self.populated = True
- return True
- def getItemData(self):
- return self.itemData
- def appendChild(self, child):
- self.childItems.append(child)
- child.deleted.connect(self.childRemoved)
- def child(self, row):
- return self.childItems[row]
- def removeChild(self, row):
- if row >= 0 and row < len(self.childItems):
- self.childItems[row].itemData.deleteLater()
- self.childItems[row].deleted.disconnect(self.childRemoved)
- del self.childItems[row]
- def childCount(self):
- return len(self.childItems)
- def columnCount(self):
- return 1
- def row(self):
- if self.parent():
- for row, item in enumerate(self.parent().childItems):
- if item is self:
- return row
- return 0
- def data(self, column):
- return "" if column == 0 else None
- def icon(self):
- return None
- def path(self):
- pathList = []
- if self.parent():
- pathList.extend(self.parent().path())
- pathList.append(self.data(0))
- return pathList
- class PluginItem(TreeItem):
- def __init__(self, dbplugin, parent=None):
- TreeItem.__init__(self, dbplugin, parent)
- def populate(self):
- if self.populated:
- return True
- # create items for connections
- for c in self.getItemData().connections():
- ConnectionItem(c, self)
- self.populated = True
- return True
- def data(self, column):
- if column == 0:
- return self.getItemData().typeNameString()
- return None
- def icon(self):
- return self.getItemData().icon()
- def path(self):
- return [self.getItemData().typeName()]
- class ConnectionItem(TreeItem):
- def __init__(self, connection, parent=None):
- TreeItem.__init__(self, connection, parent)
- connection.changed.connect(self.itemChanged)
- connection.deleted.connect(self.itemDeleted)
- # load (shared) icon with first instance of table item
- if not hasattr(ConnectionItem, 'connectedIcon'):
- ConnectionItem.connectedIcon = QIcon(":/db_manager/icons/plugged.png")
- ConnectionItem.disconnectedIcon = QIcon(":/db_manager/icons/unplugged.png")
- def data(self, column):
- if column == 0:
- return self.getItemData().connectionName()
- return None
- def icon(self):
- return self.getItemData().connectionIcon()
- def populate(self):
- if self.populated:
- return True
- connection = self.getItemData()
- if connection.database() is None:
- # connect to database
- try:
- if not connection.connect():
- return False
- except BaseError as e:
- DlgDbError.showError(e, None)
- return False
- database = connection.database()
- database.changed.connect(self.itemChanged)
- database.deleted.connect(self.itemDeleted)
- schemas = database.schemas()
- if schemas is not None:
- for s in schemas:
- SchemaItem(s, self)
- else:
- tables = database.tables()
- for t in tables:
- TableItem(t, self)
- self.populated = True
- return True
- def isConnected(self):
- return self.getItemData().database() is not None
- # def icon(self):
- # return self.connectedIcon if self.isConnected() else self.disconnectedIcon
- class SchemaItem(TreeItem):
- def __init__(self, schema, parent):
- TreeItem.__init__(self, schema, parent)
- schema.changed.connect(self.itemChanged)
- schema.deleted.connect(self.itemDeleted)
- # load (shared) icon with first instance of schema item
- if not hasattr(SchemaItem, 'schemaIcon'):
- SchemaItem.schemaIcon = QIcon(":/db_manager/icons/namespace.png")
- def data(self, column):
- if column == 0:
- return self.getItemData().name
- return None
- def icon(self):
- return self.schemaIcon
- def populate(self):
- if self.populated:
- return True
- for t in self.getItemData().tables():
- TableItem(t, self)
- self.populated = True
- return True
- class TableItem(TreeItem):
- def __init__(self, table, parent):
- TreeItem.__init__(self, table, parent)
- table.changed.connect(self.itemChanged)
- table.deleted.connect(self.itemDeleted)
- self.populate()
- # load (shared) icon with first instance of table item
- if not hasattr(TableItem, 'tableIcon'):
- TableItem.tableIcon = QgsApplication.getThemeIcon("/mIconTableLayer.svg")
- TableItem.viewIcon = QIcon(":/db_manager/icons/view.png")
- TableItem.viewMaterializedIcon = QIcon(":/db_manager/icons/view_materialized.png")
- TableItem.layerPointIcon = QgsApplication.getThemeIcon("/mIconPointLayer.svg")
- TableItem.layerLineIcon = QgsApplication.getThemeIcon("/mIconLineLayer.svg")
- TableItem.layerPolygonIcon = QgsApplication.getThemeIcon("/mIconPolygonLayer.svg")
- TableItem.layerRasterIcon = QgsApplication.getThemeIcon("/mIconRasterLayer.svg")
- TableItem.layerUnknownIcon = QIcon(":/db_manager/icons/layer_unknown.png")
- def data(self, column):
- if column == 0:
- return self.getItemData().name
- elif column == 1:
- if self.getItemData().type == Table.VectorType:
- return self.getItemData().geomType
- return None
- def icon(self):
- if self.getItemData().type == Table.VectorType:
- geom_type = self.getItemData().geomType
- if geom_type is not None:
- if geom_type.find('POINT') != -1:
- return self.layerPointIcon
- elif geom_type.find('LINESTRING') != -1 or geom_type in ('CIRCULARSTRING', 'COMPOUNDCURVE', 'MULTICURVE'):
- return self.layerLineIcon
- elif geom_type.find('POLYGON') != -1 or geom_type == 'MULTISURFACE':
- return self.layerPolygonIcon
- return self.layerUnknownIcon
- elif self.getItemData().type == Table.RasterType:
- return self.layerRasterIcon
- if self.getItemData().isView:
- if hasattr(self.getItemData(), '_relationType') and self.getItemData()._relationType == 'm':
- return self.viewMaterializedIcon
- else:
- return self.viewIcon
- return self.tableIcon
- def path(self):
- pathList = []
- if self.parent():
- pathList.extend(self.parent().path())
- if self.getItemData().type == Table.VectorType:
- pathList.append("%s::%s" % (self.data(0), self.getItemData().geomColumn))
- else:
- pathList.append(self.data(0))
- return pathList
- class DBModel(QAbstractItemModel):
- importVector = pyqtSignal(QgsVectorLayer, Database, QgsDataSourceUri, QModelIndex)
- notPopulated = pyqtSignal(QModelIndex)
- def __init__(self, parent=None):
- global isImportVectorAvail
- QAbstractItemModel.__init__(self, parent)
- self.treeView = parent
- self.header = [self.tr('Databases')]
- if isImportVectorAvail:
- self.importVector.connect(self.vectorImport)
- self.hasSpatialiteSupport = "spatialite" in supportedDbTypes()
- self.hasGPKGSupport = "gpkg" in supportedDbTypes()
- self.rootItem = TreeItem(None, None)
- for dbtype in supportedDbTypes():
- dbpluginclass = createDbPlugin(dbtype)
- item = PluginItem(dbpluginclass, self.rootItem)
- item.changed.connect(partial(self.refreshItem, item))
- def refreshItem(self, item):
- if isinstance(item, TreeItem):
- # find the index for the tree item using the path
- index = self._rPath2Index(item.path())
- else:
- # find the index for the db item
- index = self._rItem2Index(item)
- if index.isValid():
- self._refreshIndex(index)
- else:
- qDebug("invalid index")
- def _rItem2Index(self, item, parent=None):
- if parent is None:
- parent = QModelIndex()
- if item == self.getItem(parent):
- return parent
- if not parent.isValid() or parent.internalPointer().populated:
- for i in range(self.rowCount(parent)):
- index = self.index(i, 0, parent)
- index = self._rItem2Index(item, index)
- if index.isValid():
- return index
- return QModelIndex()
- def _rPath2Index(self, path, parent=None, n=0):
- if parent is None:
- parent = QModelIndex()
- if path is None or len(path) == 0:
- return parent
- for i in range(self.rowCount(parent)):
- index = self.index(i, 0, parent)
- if self._getPath(index)[n] == path[0]:
- return self._rPath2Index(path[1:], index, n + 1)
- return parent
- def getItem(self, index):
- if not index.isValid():
- return None
- return index.internalPointer().getItemData()
- def _getPath(self, index):
- if not index.isValid():
- return None
- return index.internalPointer().path()
- def columnCount(self, parent):
- return 1
- def data(self, index, role):
- if not index.isValid():
- return None
- if role == Qt.DecorationRole and index.column() == 0:
- icon = index.internalPointer().icon()
- if icon:
- return icon
- if role != Qt.DisplayRole and role != Qt.EditRole:
- return None
- retval = index.internalPointer().data(index.column())
- return retval
- def flags(self, index):
- global isImportVectorAvail
- if not index.isValid():
- return Qt.NoItemFlags
- flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
- if index.column() == 0:
- item = index.internalPointer()
- if isinstance(item, SchemaItem) or isinstance(item, TableItem):
- flags |= Qt.ItemIsEditable
- if isinstance(item, TableItem):
- flags |= Qt.ItemIsDragEnabled
- # vectors/tables can be dropped on connected databases to be imported
- if isImportVectorAvail:
- if isinstance(item, ConnectionItem) and item.populated:
- flags |= Qt.ItemIsDropEnabled
- if isinstance(item, (SchemaItem, TableItem)):
- flags |= Qt.ItemIsDropEnabled
- # SL/Geopackage db files can be dropped everywhere in the tree
- if self.hasSpatialiteSupport or self.hasGPKGSupport:
- flags |= Qt.ItemIsDropEnabled
- return flags
- def headerData(self, section, orientation, role):
- if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(self.header):
- return self.header[section]
- return None
- def index(self, row, column, parent):
- if not self.hasIndex(row, column, parent):
- return QModelIndex()
- parentItem = parent.internalPointer() if parent.isValid() else self.rootItem
- childItem = parentItem.child(row)
- if childItem:
- return self.createIndex(row, column, childItem)
- return QModelIndex()
- def parent(self, index):
- if not index.isValid():
- return QModelIndex()
- childItem = index.internalPointer()
- parentItem = childItem.parent()
- if parentItem == self.rootItem:
- return QModelIndex()
- return self.createIndex(parentItem.row(), 0, parentItem)
- def rowCount(self, parent):
- parentItem = parent.internalPointer() if parent.isValid() else self.rootItem
- if not parentItem.populated:
- self._refreshIndex(parent, True)
- return parentItem.childCount()
- def hasChildren(self, parent):
- parentItem = parent.internalPointer() if parent.isValid() else self.rootItem
- return parentItem.childCount() > 0 or not parentItem.populated
- def setData(self, index, value, role):
- if role != Qt.EditRole or index.column() != 0:
- return False
- item = index.internalPointer()
- new_value = str(value)
- if isinstance(item, SchemaItem) or isinstance(item, TableItem):
- obj = item.getItemData()
- # rename schema or table or view
- if new_value == obj.name:
- return False
- with OverrideCursor(Qt.WaitCursor):
- try:
- obj.rename(new_value)
- self._onDataChanged(index)
- except BaseError as e:
- DlgDbError.showError(e, self.treeView)
- return False
- else:
- return True
- return False
- def removeRows(self, row, count, parent):
- self.beginRemoveRows(parent, row, count + row - 1)
- item = parent.internalPointer()
- for i in range(row, count + row):
- item.removeChild(row)
- self.endRemoveRows()
- def _refreshIndex(self, index, force=False):
- with OverrideCursor(Qt.WaitCursor):
- try:
- item = index.internalPointer() if index.isValid() else self.rootItem
- prevPopulated = item.populated
- if prevPopulated:
- self.removeRows(0, self.rowCount(index), index)
- item.populated = False
- if prevPopulated or force:
- if item.populate():
- for child in item.childItems:
- child.changed.connect(partial(self.refreshItem, child))
- self._onDataChanged(index)
- else:
- self.notPopulated.emit(index)
- except BaseError:
- item.populated = False
- def _onDataChanged(self, indexFrom, indexTo=None):
- if indexTo is None:
- indexTo = indexFrom
- self.dataChanged.emit(indexFrom, indexTo)
- QGIS_URI_MIME = "application/x-vnd.qgis.qgis.uri"
- def mimeTypes(self):
- return ["text/uri-list", self.QGIS_URI_MIME]
- def mimeData(self, indexes):
- mimeData = QMimeData()
- encodedData = QByteArray()
- stream = QDataStream(encodedData, QIODevice.WriteOnly)
- for index in indexes:
- if not index.isValid():
- continue
- if not isinstance(index.internalPointer(), TableItem):
- continue
- table = self.getItem(index)
- stream.writeQString(table.mimeUri())
- mimeData.setData(self.QGIS_URI_MIME, encodedData)
- return mimeData
- def dropMimeData(self, data, action, row, column, parent):
- global isImportVectorAvail
- if action == Qt.IgnoreAction:
- return True
- # vectors/tables to be imported must be dropped on connected db, schema or table
- canImportLayer = isImportVectorAvail and parent.isValid() and \
- (isinstance(parent.internalPointer(), (SchemaItem, TableItem)) or
- (isinstance(parent.internalPointer(), ConnectionItem) and parent.internalPointer().populated))
- added = 0
- if data.hasUrls():
- for u in data.urls():
- filename = u.toLocalFile()
- if filename == "":
- continue
- if self.hasSpatialiteSupport:
- from .db_plugins.spatialite.connector import SpatiaLiteDBConnector
- if SpatiaLiteDBConnector.isValidDatabase(filename):
- # retrieve the SL plugin tree item using its path
- index = self._rPath2Index(["spatialite"])
- if not index.isValid():
- continue
- item = index.internalPointer()
- conn_name = QFileInfo(filename).fileName()
- uri = QgsDataSourceUri()
- uri.setDatabase(filename)
- item.getItemData().addConnection(conn_name, uri)
- item.changed.emit()
- added += 1
- continue
- if canImportLayer:
- if QgsRasterLayer.isValidRasterFileName(filename):
- layerType = 'raster'
- providerKey = 'gdal'
- else:
- layerType = 'vector'
- providerKey = 'ogr'
- layerName = QFileInfo(filename).completeBaseName()
- if self.importLayer(layerType, providerKey, layerName, filename, parent):
- added += 1
- if data.hasFormat(self.QGIS_URI_MIME):
- for uri in QgsMimeDataUtils.decodeUriList(data):
- if canImportLayer:
- if self.importLayer(uri.layerType, uri.providerKey, uri.name, uri.uri, parent):
- added += 1
- return added > 0
- def importLayer(self, layerType, providerKey, layerName, uriString, parent):
- global isImportVectorAvail
- if not isImportVectorAvail:
- return False
- if layerType == 'raster':
- return False # not implemented yet
- inLayer = QgsRasterLayer(uriString, layerName, providerKey)
- else:
- inLayer = QgsVectorLayer(uriString, layerName, providerKey)
- if not inLayer.isValid():
- # invalid layer
- QMessageBox.warning(None, self.tr("Invalid layer"), self.tr("Unable to load the layer {0}").format(inLayer.name()))
- return False
- # retrieve information about the new table's db and schema
- outItem = parent.internalPointer()
- outObj = outItem.getItemData()
- outDb = outObj.database()
- outSchema = None
- if isinstance(outItem, SchemaItem):
- outSchema = outObj
- elif isinstance(outItem, TableItem):
- outSchema = outObj.schema()
- # toIndex will point to the parent item of the new table
- toIndex = parent
- if isinstance(toIndex.internalPointer(), TableItem):
- toIndex = toIndex.parent()
- if inLayer.type() == inLayer.VectorLayer:
- # create the output uri
- schema = outSchema.name if outDb.schemas() is not None and outSchema is not None else ""
- pkCol = geomCol = ""
- # default pk and geom field name value
- if providerKey in ['postgres', 'spatialite']:
- inUri = QgsDataSourceUri(inLayer.source())
- pkCol = inUri.keyColumn()
- geomCol = inUri.geometryColumn()
- outUri = outDb.uri()
- outUri.setDataSource(schema, layerName, geomCol, "", pkCol)
- self.importVector.emit(inLayer, outDb, outUri, toIndex)
- return True
- return False
- def vectorImport(self, inLayer, outDb, outUri, parent):
- global isImportVectorAvail
- if not isImportVectorAvail:
- return False
- try:
- from .dlg_import_vector import DlgImportVector
- dlg = DlgImportVector(inLayer, outDb, outUri)
- QApplication.restoreOverrideCursor()
- if dlg.exec_():
- self._refreshIndex(parent)
- finally:
- inLayer.deleteLater()
|