mirror of
https://github.com/qgis/QGIS.git
synced 2025-02-27 00:33:48 -05:00
Since the majority of users of this class will be exporting an existing map layer to a data provider, the QgsVectorLayerImport name is misleading and suggests that this class is designed just to bring layers "into" QGIS. Explicitly naming the class "Exporter" should help API users discover this class. Also cleanup API and improve docs
655 lines
21 KiB
Python
655 lines
21 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
/***************************************************************************
|
|
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 builtins import str
|
|
from builtins import range
|
|
|
|
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 QgsDataSourceUri, QgsVectorLayer, QgsRasterLayer, QgsMimeDataUtils
|
|
|
|
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 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 = QIcon(":/db_manager/icons/table.png")
|
|
TableItem.viewIcon = QIcon(":/db_manager/icons/view.png")
|
|
TableItem.viewMaterializedIcon = QIcon(":/db_manager/icons/view_materialized.png")
|
|
TableItem.layerPointIcon = QIcon(":/db_manager/icons/layer_point.png")
|
|
TableItem.layerLineIcon = QIcon(":/db_manager/icons/layer_line.png")
|
|
TableItem.layerPolygonIcon = QIcon(":/db_manager/icons/layer_polygon.png")
|
|
TableItem.layerRasterIcon = QIcon(":/db_manager/icons/layer_raster.png")
|
|
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
|
|
|
|
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
try:
|
|
obj.rename(new_value)
|
|
self._onDataChanged(index)
|
|
except BaseError as e:
|
|
DlgDbError.showError(e, self.treeView)
|
|
return False
|
|
finally:
|
|
QApplication.restoreOverrideCursor()
|
|
|
|
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):
|
|
QApplication.setOverrideCursor(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
|
|
return
|
|
|
|
finally:
|
|
QApplication.restoreOverrideCursor()
|
|
|
|
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()
|