[DBManager] add SL/Geopackage connection by drag&drop (follow b11f67b4f5), improve GPKG support:

- recognize tables with geometry,
- load GPGK layers to canvas from context menu,
- disable table editing
This commit is contained in:
Giuseppe Sucameli 2015-09-03 01:22:54 +02:00
parent 7d79a73f58
commit 03916d549a
7 changed files with 286 additions and 94 deletions

View File

@ -284,6 +284,8 @@ class DBModel(QAbstractItemModel):
if self.isImportVectorAvail:
self.connect(self, SIGNAL("importVector"), self.importVector)
self.hasSpatialiteSupport = "spatialite" in supportedDbTypes()
self.rootItem = TreeItem(None, None)
for dbtype in supportedDbTypes():
dbpluginclass = createDbPlugin(dbtype)
@ -373,13 +375,18 @@ class DBModel(QAbstractItemModel):
if isinstance(item, TableItem):
flags |= Qt.ItemIsDragEnabled
if self.isImportVectorAvail: # allow to import a vector layer
# vectors/tables can be dropped on connected databases to be imported
if self.isImportVectorAvail:
if isinstance(item, ConnectionItem) and item.populated:
flags |= Qt.ItemIsDropEnabled
if isinstance(item, SchemaItem) or isinstance(item, TableItem):
if isinstance(item, (SchemaItem, TableItem)):
flags |= Qt.ItemIsDropEnabled
# SL/Geopackage db files can be dropped everywhere in the tree
if self.hasSpatialiteSupport:
flags |= Qt.ItemIsDropEnabled
return flags
def headerData(self, section, orientation, role):
@ -508,8 +515,10 @@ class DBModel(QAbstractItemModel):
if action == Qt.IgnoreAction:
return True
if not self.isImportVectorAvail:
return False
# vectors/tables to be imported must be dropped on connected db, schema or table
canImportLayer = self.isImportVectorAvail and parent.isValid() and \
( isinstance(parent.internalPointer(), (SchemaItem, TableItem)) or \
( isinstance(parent.internalPointer(), ConnectionItem) and parent.internalPointer().populated ) )
added = 0
@ -518,22 +527,42 @@ class DBModel(QAbstractItemModel):
filename = u.toLocalFile()
if filename == "":
continue
if qgis.core.QgsRasterLayer.isValidRasterFileName(filename):
layerType = 'raster'
providerKey = 'gdal'
else:
layerType = 'vector'
providerKey = 'ogr'
layerName = QFileInfo(filename).completeBaseName()
if self.hasSpatialiteSupport:
from .db_plugins.spatialite.connector import SpatiaLiteDBConnector
if self.importLayer(layerType, providerKey, layerName, filename, parent):
added += 1
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 = qgis.core.QgsDataSourceURI()
uri.setDatabase(filename)
item.getItemData().addConnection(conn_name, uri)
item.emit(SIGNAL('itemChanged'), item)
added += 1
continue
if canImportLayer:
if qgis.core.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 qgis.core.QgsMimeDataUtils.decodeUriList(data):
if self.importLayer(uri.layerType, uri.providerKey, uri.name, uri.uri, parent):
added += 1
if canImportLayer:
if self.importLayer(uri.layerType, uri.providerKey, uri.name, uri.uri, parent):
added += 1
return added > 0

View File

@ -97,6 +97,9 @@ class DBPlugin(QObject):
return DatabaseInfo(None)
def connect(self, parent=None):
raise NotImplemented
def connectToUri(self, uri):
self.db = self.databasesFactory(self, uri)
if self.db:
@ -111,6 +114,17 @@ class DBPlugin(QObject):
return self.connectToUri(uri)
return self.connect(self.parent())
def remove(self):
settings = QSettings()
settings.beginGroup(u"/%s/%s" % (self.connectionSettingsKey(), self.connectionName()))
settings.remove("")
self.emit(SIGNAL('deleted'))
return True
@classmethod
def addConnection(self, conn_name, uri):
raise NotImplemented
@classmethod
def icon(self):
return None
@ -135,11 +149,6 @@ class DBPlugin(QObject):
# return the key used to store the connections in settings
pass
@classmethod
def connectionSettingsFileKey(self):
# return the filekey for the settings
pass
@classmethod
def connections(self):
# get the list of connections
@ -154,6 +163,24 @@ class DBPlugin(QObject):
def databasesFactory(self, connection, uri):
return None
@classmethod
def addConnectionActionSlot(self, item, action, parent):
raise NotImplemented
def removeActionSlot(self, item, action, parent):
QApplication.restoreOverrideCursor()
try:
res = QMessageBox.question(parent, QApplication.translate("DBManagerPlugin", "hey!"),
QApplication.translate("DBManagerPlugin",
"Really remove connection to %s?") % item.connectionName(),
QMessageBox.Yes | QMessageBox.No)
if res != QMessageBox.Yes:
return
finally:
QApplication.setOverrideCursor(Qt.WaitCursor)
item.remove()
class DbItemObject(QObject):
@ -207,6 +234,13 @@ class Database(DbItemObject):
def publicUri(self):
return self.connector.publicUri()
def delete(self):
self.aboutToChange()
ret = self.connection().remove()
if ret is not False:
self.emit(SIGNAL('deleted'))
return ret
def info(self):
from .info_model import DatabaseInfo
@ -636,9 +670,6 @@ class Table(DbItemObject):
def mimeUri(self):
layerType = "raster" if self.type == Table.RasterType else "vector"
if self.database().dbplugin().typeName() == "spatialite" and self.database().connector.isgpkg():
url = unicode(self.database().connector._connectionInfo() + "|layername=" + self.name)
return u"%s:%s:%s:%s" % (layerType, "ogr", self.name, url)
return u"%s:%s:%s:%s" % (layerType, self.database().dbplugin().providerName(), self.name, self.uri().uri())
def toMapLayer(self):

View File

@ -64,10 +64,6 @@ class PostGisDBPlugin(DBPlugin):
def connectionSettingsKey(self):
return '/PostgreSQL/connections'
@classmethod
def connectionSettingsFileKey(self):
return "database"
def databasesFactory(self, connection, uri):
return PGDatabase(connection, uri)
@ -284,6 +280,7 @@ class PGRasterTable(PGTable, RasterTable):
return gdalUri
def mimeUri(self):
# QGIS has no provider for PGRasters, let's use GDAL
uri = u"raster:gdal:%s:%s" % (self.name, re.sub(":", "\:", self.gdalUri()))
return uri

View File

@ -50,12 +50,22 @@ class SpatiaLiteDBConnector(DBConnector):
self._checkSpatial()
self._checkRaster()
self._checkGeometryColumnsTable()
self._checkRastersTable()
self._checkGeopackage()
def _connectionInfo(self):
return unicode(self.dbname)
@classmethod
def isValidDatabase(self, path):
if not QFile.exists(path):
return False
try:
conn = sqlite.connect(path)
except self.connection_error_types() as e:
return False
conn.close()
return True
def _checkSpatial(self):
""" check if it's a valid spatialite db """
self.has_spatial = self._checkGeometryColumnsTable()
@ -63,9 +73,14 @@ class SpatiaLiteDBConnector(DBConnector):
def _checkRaster(self):
""" check if it's a rasterite db """
self.has_raster = self._checkRastersTable()
self.has_raster = self._checkRasterTables()
return self.has_raster
def _checkGeopackage(self):
""" check if it's a geopackage db """
self.is_gpkg = self._checkGeopackageTables()
return self.is_gpkg
def _checkGeometryColumnsTable(self):
try:
c = self._get_cursor()
@ -80,13 +95,40 @@ class SpatiaLiteDBConnector(DBConnector):
self.has_geometry_columns_access = self.has_geometry_columns
return self.has_geometry_columns
def _checkRastersTable(self):
def _checkRasterTables(self):
c = self._get_cursor()
sql = u"SELECT count(*) = 3 FROM sqlite_master WHERE name IN ('layer_params', 'layer_statistics', 'raster_pyramids')"
self._execute(c, sql)
ret = c.fetchone()
return ret and ret[0]
def _checkGeopackageTables(self):
try:
sql = u"SELECT HasGeoPackage()"
result = self._execute(None, sql).fetchone()[0] == 1
except ConnectionError:
result = False
if result:
try:
sql = u"SELECT CheckGeoPackageMetaData()"
result = self._execute(None, sql).fetchone()[0] == 1
except ConnectionError:
result = False
else:
# Spatialite < 4.2 has no GeoPackage support, check for filename and GPKG layout
ver = map(int, self.getInfo()[0].split('.')[0:2])
if ver[0] < 4 or (ver[0] == 4 and ver[1] < 2):
hasGpkgFileExt = self.dbname[-5:] == ".gpkg" or self.dbname[-11:] == ".geopackage"
sql = u"SELECT count(*) = 3 FROM sqlite_master WHERE name IN ('gpkg_geometry_columns', 'gpkg_spatial_ref_sys', 'gpkg_contents')"
ret = self._execute(None, sql).fetchone()
hasGpkgLayout = ret and ret[0]
result = hasGpkgFileExt and hasGpkgLayout
return result
def getInfo(self):
c = self._get_cursor()
self._execute(c, u"SELECT sqlite_version()")
@ -98,7 +140,7 @@ class SpatiaLiteDBConnector(DBConnector):
- geos version
- proj version
"""
if not self.has_spatial:
if not self.has_spatial and not self.is_gpkg:
return
c = self._get_cursor()
@ -126,14 +168,8 @@ class SpatiaLiteDBConnector(DBConnector):
def hasCreateSpatialViewSupport(self):
return True
def isgpkg(self):
info = map(int, self.getInfo()[0].split('.')[0:2])
if info[0] < 4 or (info[0] == 4 and info[1] < 2):
result = self.uri().database()[-5:] == ".gpkg"
else:
sql = u"SELECT HasGeoPackage()"
result = self._execute(None, sql).fetchone()[0] == 1
return result
def isGpkg(self):
return self.is_gpkg
def fieldTypes(self):
return [
@ -225,13 +261,9 @@ class SpatiaLiteDBConnector(DBConnector):
srid
"""
if not self.has_geometry_columns:
return []
c = self._get_cursor()
if self.has_spatialite4:
cols = """CASE geometry_type % 10
if self.has_geometry_columns:
if self.has_spatialite4:
cols = """CASE geometry_type % 10
WHEN 1 THEN 'POINT'
WHEN 2 THEN 'LINESTRING'
WHEN 3 THEN 'POLYGON'
@ -247,15 +279,27 @@ class SpatiaLiteDBConnector(DBConnector):
WHEN 3 THEN 'XYZM'
ELSE NULL
END AS coord_dimension"""
else:
cols = "g.type,g.coord_dimension"
else:
cols = "g.type,g.coord_dimension"
# get geometry info from geometry_columns if exists
sql = u"""SELECT m.name, m.type = 'view', g.f_table_name, g.f_geometry_column, %s, g.srid
# get geometry info from geometry_columns if exists
sql = u"""SELECT m.name, m.type = 'view', g.f_table_name, g.f_geometry_column, %s, g.srid
FROM sqlite_master AS m JOIN geometry_columns AS g ON upper(m.name) = upper(g.f_table_name)
WHERE m.type in ('table', 'view')
ORDER BY m.name, g.f_geometry_column""" % cols
elif self.is_gpkg:
# get info from gpkg_geometry_columns table
dim = " 'XY' || CASE z WHEN 1 THEN 'Z' END || CASE m WHEN 1 THEN 'M' END AS coord_dimension "
sql = u"""SELECT m.name, m.type = 'view', g.table_name, g.column_name, g.geometry_type_name AS gtype, %s, g.srs_id
FROM sqlite_master AS m JOIN gpkg_geometry_columns AS g ON upper(m.name) = upper(g.table_name)
WHERE m.type in ('table', 'view')
ORDER BY m.name, g.column_name""" % dim
else:
return []
c = self._get_cursor()
self._execute(c, sql)
items = []
@ -277,6 +321,8 @@ class SpatiaLiteDBConnector(DBConnector):
srid
"""
if self.is_gpkg:
return [] # Not implemented
if not self.has_geometry_columns:
return []
if not self.has_raster:
@ -375,7 +421,10 @@ class SpatiaLiteDBConnector(DBConnector):
return ret[0] if ret is not None else None
def getSpatialRefInfo(self, srid):
sql = u"SELECT ref_sys_name FROM spatial_ref_sys WHERE srid = %s" % self.quoteString(srid)
if self.is_gpkg:
sql = u"SELECT srs_name FROM gpkg_spatial_ref_sys WHERE srs_id = %s" % self.quoteString(srid)
else:
sql = u"SELECT ref_sys_name FROM spatial_ref_sys WHERE srid = %s" % self.quoteString(srid)
c = self._execute(None, sql)
ret = c.fetchone()
return ret[0] if ret is not None else None
@ -428,6 +477,8 @@ class SpatiaLiteDBConnector(DBConnector):
""" delete table from the database """
if self.isRasterTable(table):
return False
if self.is_gpkg:
return False # Not implemented
c = self._get_cursor()
sql = u"DROP TABLE %s" % self.quoteId(table)
@ -441,6 +492,8 @@ class SpatiaLiteDBConnector(DBConnector):
""" delete all rows from table """
if self.isRasterTable(table):
return False
if self.is_gpkg:
return False # Not implemented
sql = u"DELETE FROM %s" % self.quoteId(table)
self._execute_and_commit(sql)
@ -453,6 +506,8 @@ class SpatiaLiteDBConnector(DBConnector):
if self.isRasterTable(table):
return False
if self.is_gpkg:
return False # Not implemented
c = self._get_cursor()
@ -492,6 +547,9 @@ class SpatiaLiteDBConnector(DBConnector):
return self.renameTable(view, new_name)
def createSpatialView(self, view, query):
if self.is_gpkg:
return False # Not implemented
self.createView(view, query)
# get type info about the view
sql = u"PRAGMA table_info(%s)" % self.quoteString(view)
@ -570,6 +628,9 @@ class SpatiaLiteDBConnector(DBConnector):
return False # column editing not supported
def isGeometryColumn(self, table, column):
if self.is_gpkg:
return False # Not implemented
c = self._get_cursor()
schema, tablename = self.getSchemaTableName(table)
sql = u"SELECT count(*) > 0 FROM geometry_columns WHERE upper(f_table_name) = upper(%s) AND upper(f_geometry_column) = upper(%s)" % (
@ -578,6 +639,9 @@ class SpatiaLiteDBConnector(DBConnector):
return c.fetchone()[0] == 't'
def addGeometryColumn(self, table, geom_column='geometry', geom_type='POINT', srid=-1, dim=2):
if self.is_gpkg:
return False # Not implemented
schema, tablename = self.getSchemaTableName(table)
sql = u"SELECT AddGeometryColumn(%s, %s, %d, %s, %s)" % (
self.quoteString(tablename), self.quoteString(geom_column), srid, self.quoteString(geom_type), dim)
@ -614,6 +678,8 @@ class SpatiaLiteDBConnector(DBConnector):
def createSpatialIndex(self, table, geom_column='geometry'):
if self.isRasterTable(table):
return False
if self.is_gpkg:
return False # Not implemented
schema, tablename = self.getSchemaTableName(table)
sql = u"SELECT CreateSpatialIndex(%s, %s)" % (self.quoteString(tablename), self.quoteString(geom_column))
@ -622,6 +688,8 @@ class SpatiaLiteDBConnector(DBConnector):
def deleteSpatialIndex(self, table, geom_column='geometry'):
if self.isRasterTable(table):
return False
if self.is_gpkg:
return False # Not implemented
schema, tablename = self.getSchemaTableName(table)
try:
@ -635,6 +703,8 @@ class SpatiaLiteDBConnector(DBConnector):
self.deleteTable(idx_table_name)
def hasSpatialIndex(self, table, geom_column='geometry'):
if self.is_gpkg:
return False # Not implemented
if not self.has_geometry_columns or self.isRasterTable(table):
return False
c = self._get_cursor()

View File

@ -44,5 +44,29 @@ class SLDatabaseInfo(DatabaseInfo):
]
return HtmlTable(tbl)
def spatialInfo(self):
ret = []
info = self.db.connector.getSpatialInfo()
if info is None:
return
tbl = [
(QApplication.translate("DBManagerPlugin", "Library:"), info[0]),
("GEOS:", info[1]),
("Proj:", info[2])
]
ret.append(HtmlTable(tbl))
if self.db.connector.is_gpkg:
pass
elif not self.db.connector.has_geometry_columns:
ret.append(HtmlParagraph(
QApplication.translate("DBManagerPlugin", "<warning> geometry_columns table doesn't exist!\n"
"This table is essential for many GIS applications for enumeration of tables.")))
return ret
def privilegesDetails(self):
return None

View File

@ -62,10 +62,6 @@ class SpatiaLiteDBPlugin(DBPlugin):
def connectionSettingsKey(self):
return '/SpatiaLite/connections'
@classmethod
def connectionSettingsFileKey(self):
return "sqlitepath"
def databasesFactory(self, connection, uri):
return SLDatabase(connection, uri)
@ -85,6 +81,31 @@ class SpatiaLiteDBPlugin(DBPlugin):
uri.setDatabase(database)
return self.connectToUri(uri)
@classmethod
def addConnection(self, conn_name, uri):
settings = QSettings()
settings.beginGroup(u"/%s/%s" % (self.connectionSettingsKey(), conn_name))
settings.setValue("sqlitepath", uri.database())
return True
@classmethod
def addConnectionActionSlot(self, item, action, parent, index):
QApplication.restoreOverrideCursor()
try:
filename = QFileDialog.getOpenFileName(self, "Choose Sqlite/Spatialite/Geopackage file")
if not filename:
return
finally:
QApplication.setOverrideCursor(Qt.WaitCursor)
conn_name = QFileInfo(filepath).fileName()
uri = qgis.core.QgsDataSourceURI()
uri.setDatabase(filepath)
self.addConnection(conn_name, uri)
index.internalPointer().emit(SIGNAL('itemChanged'))
class SLDatabase(Database):
@ -153,7 +174,7 @@ class SLDatabase(Database):
return True
def spatialIndexClause(self, src_table, src_column, dest_table, dest_column):
return """"%s".ROWID IN (\nSELECT ROWID FROM SpatialIndex WHERE f_table_name='%s' AND search_frame="%s"."%s") """ % (src_table, src_table, dest_table, dest_column)
return u""" "%s".ROWID IN (\nSELECT ROWID FROM SpatialIndex WHERE f_table_name='%s' AND search_frame="%s"."%s") """ % (src_table, src_table, dest_table, dest_column)
class SLTable(Table):
@ -162,6 +183,29 @@ class SLTable(Table):
Table.__init__(self, db, None)
self.name, self.isView, self.isSysTable = row
def ogrUri(self):
ogrUri = u"%s|layername=%s" % (self.uri().database(), self.name)
return ogrUri
def mimeUri(self):
if self.database().connector.isGpkg():
# QGIS has no provider to load Geopackage vectors, let's use OGR
return u"vector:ogr:%s:%s" % (self.name, self.ogrUri())
return VectorTable.mimeUri(self)
def toMapLayer(self):
from qgis.core import QgsVectorLayer
if self.database().connector.isGpkg():
# QGIS has no provider to load Geopackage vectors, let's use OGR
provider = "ogr"
uri = self.ogrUri()
else:
provider = self.database().dbplugin().providerName()
uri = self.uri().uri()
return QgsVectorLayer(uri, self.name, provider)
def tableFieldsFactory(self, row, table):
return SLTableField(row, table)
@ -231,19 +275,30 @@ class SLRasterTable(SLTable, RasterTable):
#from .info_model import SLRasterTableInfo
#return SLRasterTableInfo(self)
def gdalUri(self):
uri = self.database().uri()
gdalUri = u'RASTERLITE:%s,table=%s' % (uri.database(), self.prefixName)
def rasterliteGdalUri(self):
gdalUri = u'RASTERLITE:%s,table=%s' % (self.uri().database(), self.prefixName)
return gdalUri
def mimeUri(self):
uri = u"raster:gdal:%s:%s" % (self.name, self.gdalUri())
if self.database().connector.isGpkg():
# QGIS has no provider to load Geopackage rasters, let's use GDAL
uri = u"raster:gdal:%s:%s" % (self.name, self.uri().database())
else:
# QGIS has no provider to load Rasterlite rasters, let's use GDAL
uri = u"raster:gdal:%s:%s" % (self.name, self.rasterliteGdalUri())
return uri
def toMapLayer(self):
from qgis.core import QgsRasterLayer, QgsContrastEnhancement
rl = QgsRasterLayer(self.gdalUri(), self.name)
if self.database().connector.isGpkg():
# QGIS has no provider to load Geopackage rasters, let's use GDAL
uri = self.ogrUri()
else:
# QGIS has no provider to load Rasterlite rasters, let's use GDAL
uri = self.rasterliteGdalUri()
rl = QgsRasterLayer(uri, self.name)
if rl.isValid():
rl.setContrastEnhancement(QgsContrastEnhancement.StretchToMinimumMaximum)
return rl

View File

@ -26,7 +26,7 @@ from PyQt4.QtGui import QWidget, QTreeView, QMenu, QLabel, QFileDialog
from qgis.core import QgsMapLayerRegistry, QgsMessageLog
from qgis.gui import QgsMessageBar, QgsMessageBarItem
from .db_model import DBModel
from .db_model import DBModel, PluginItem
from .db_plugins.plugin import DBPlugin, Schema, Table
@ -94,12 +94,12 @@ class DBTree(QTreeView):
return item
return None
def openConnection(self):
index = self.selectedIndexes()[0]
if index:
if index.data() != "PostGIS":
filename = QFileDialog.getOpenFileName(self, "Open File")
self.model().addConnection(filename, index)
def newConnection(self):
index = self.currentIndex()
if not index.isValid() or not isinstance(index.internalPointer(), PluginItem):
return
item = self.currentItem()
self.mainWindow.invokeCallback(item.addConnectionActionSlot, index)
def itemChanged(self, index):
self.setCurrentIndex(index)
@ -131,12 +131,13 @@ class DBTree(QTreeView):
menu.addSeparator()
menu.addAction(self.tr("Add to canvas"), self.addLayer)
elif isinstance(item, DBPlugin) and item.database() is not None:
menu.addAction(self.tr("Re-connect"), self.reconnect)
menu.addAction(self.tr("Delete"), self.delActionSlot)
elif isinstance(item, DBPlugin):
if item.database() is not None:
menu.addAction(self.tr("Re-connect"), self.reconnect)
menu.addAction(self.tr("Remove"), self.delete)
elif not index.parent().data():
menu.addAction(self.tr("New Connection..."), self.openConnection)
elif not index.parent().isValid() and item.typeName() == "spatialite":
menu.addAction(self.tr("New Connection..."), self.newConnection)
if not menu.isEmpty():
menu.exec_(ev.globalPos())
@ -144,32 +145,16 @@ class DBTree(QTreeView):
menu.deleteLater()
def rename(self):
index = self.currentIndex()
item = self.model().getItem(index)
item = self.currentItem()
if isinstance(item, (Table, Schema)):
self.edit(index)
def delActionSlot(self):
db = self.currentDatabase()
path = db.uri().database()
connkey = db.connection().connectionSettingsKey()
self.deletedb(path, connkey)
index = self.currentIndex().parent()
self.setCurrentIndex(index)
self.mainWindow.refreshActionSlot()
def deletedb(self, path, conn):
paths = path.split("/")
path = paths[-1]
s = QSettings()
s.beginGroup("/%s/%s" % (conn, path))
s.remove("")
self.edit(self.currentIndex())
def delete(self):
item = self.currentItem()
if isinstance(item, (Table, Schema)):
self.mainWindow.invokeCallback(item.database().deleteActionSlot)
elif isinstance(item, DBPlugin):
self.mainWindow.invokeCallback(item.removeActionSlot)
def addLayer(self):
table = self.currentTable()
@ -193,3 +178,4 @@ class DBTree(QTreeView):
db = self.currentDatabase()
if db is not None:
self.mainWindow.invokeCallback(db.reconnectActionSlot)