Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[processing] allow output directly on Spatialite tables #2423

Merged
merged 1 commit into from Nov 9, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
120 changes: 120 additions & 0 deletions python/plugins/processing/algs/qgis/spatialite_utils.py
@@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-

"""
***************************************************************************
spatialite_utils.py
---------------------
Date : November 2015
Copyright : (C) 2015 by René-Luc Dhont
Email : volayaf at gmail dot 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. *
* *
***************************************************************************
"""

__author__ = 'René-Luc Dhont'
__date__ = 'November 2015'
__copyright__ = '(C) 2015, René-Luc Dhont'

# This will get replaced with a git SHA1 when you do a git archive

__revision__ = '$Format:%H$'

from pyspatialite import dbapi2 as sqlite

class DbError(Exception):

def __init__(self, message, query=None):
# Save error. funny that the variables are in utf-8
self.message = unicode(message, 'utf-8')
self.query = (unicode(query, 'utf-8') if query is not None else None)

def __str__(self):
return 'MESSAGE: %s\nQUERY: %s' % (self.message, self.query)

class GeoDB:

def __init__(self, uri=None):
self.uri = uri
self.dbname = uri.database()

try:
self.con = sqlite.connect(self.con_info())

except (sqlite.InterfaceError, sqlite.OperationalError) as e:
raise DbError(e.message)

self.has_spatialite = self.check_spatialite()
if not self.has_spatialite:
self.has_spatialite = self.init_spatialite()

def con_info(self):
return unicode(self.dbname)

def init_spatialite(self):
# Get spatialite version
c = self.con.cursor()
try:
self._exec_sql(c, u'SELECT spatialite_version()')
rep = c.fetchall()
v = [int(a) for a in rep[0][0].split('.')]
vv = v[0] * 100000 + v[1] * 1000 + v[2] * 10

# Add spatialite support
if vv >= 401000:
# 4.1 and above
sql = "SELECT initspatialmetadata(1)"
else:
# Under 4.1
sql = "SELECT initspatialmetadata()"
self._exec_sql_and_commit(sql)
except:
return False
finally:
self.con.close()

try:
self.con = sqlite.connect(self.con_info())

except (sqlite.InterfaceError, sqlite.OperationalError) as e:
raise DbError(e.message)

return self.check_spatialite()

def check_spatialite(self):
try:
c = self.con.cursor()
self._exec_sql(c, u"SELECT CheckSpatialMetaData()")
v = c.fetchone()[0]
self.has_geometry_columns = v == 1 or v == 3
self.has_spatialite4 = v == 3
except Exception as e:
self.has_geometry_columns = False
self.has_spatialite4 = False

self.has_geometry_columns_access = self.has_geometry_columns
return self.has_geometry_columns

def _exec_sql(self, cursor, sql):
try:
cursor.execute(sql)
except (sqlite.Error, sqlite.ProgrammingError, sqlite.Warning, sqlite.InterfaceError, sqlite.OperationalError) as e:
raise DbError(e.message, sql)

def _exec_sql_and_commit(self, sql):
"""Tries to execute and commit some action, on error it rolls
back the change.
"""

try:
c = self.con.cursor()
self._exec_sql(c, sql)
self.con.commit()
except DbError as e:
self.con.rollback()
raise
42 changes: 42 additions & 0 deletions python/plugins/processing/gui/OutputSelectionPanel.py
Expand Up @@ -83,6 +83,10 @@ def selectOutput(self):
self.tr('Save to memory layer'), self.btnSelect)
actionSaveToMemory.triggered.connect(self.saveToMemory)
popupMenu.addAction(actionSaveToMemory)
actionSaveToSpatialite = QAction(
self.tr('Save to Spatialite table...'), self.btnSelect)
actionSaveToSpatialite.triggered.connect(self.saveToSpatialite)
popupMenu.addAction(actionSaveToSpatialite)
actionSaveToPostGIS = QAction(
self.tr('Save to PostGIS table...'), self.btnSelect)
actionSaveToPostGIS.triggered.connect(self.saveToPostGIS)
Expand Down Expand Up @@ -118,6 +122,42 @@ def saveToPostGIS(self):
QgsCredentials.instance().put(connInfo, user, passwd)
self.leText.setText("postgis:" + uri.uri())

def saveToSpatialite(self):
fileFilter = self.output.tr('Spatialite files(*.sqlite)', 'OutputFile')

settings = QSettings()
if settings.contains('/Processing/LastOutputPath'):
path = settings.value('/Processing/LastOutputPath')
else:
path = ProcessingConfig.getSetting(ProcessingConfig.OUTPUT_FOLDER)

encoding = settings.value('/Processing/encoding', 'System')
fileDialog = QgsEncodingFileDialog(
self, self.tr('Save Spatialite'), path, fileFilter, encoding)
fileDialog.setFileMode(QFileDialog.AnyFile)
fileDialog.setAcceptMode(QFileDialog.AcceptSave)
fileDialog.setConfirmOverwrite(False)

if fileDialog.exec_() == QDialog.Accepted:
files = fileDialog.selectedFiles()
encoding = unicode(fileDialog.encoding())
self.output.encoding = encoding
fileName = unicode(files[0])
selectedFileFilter = unicode(fileDialog.selectedNameFilter())
if not fileName.lower().endswith(
tuple(re.findall("\*(\.[a-z]{1,10})", fileFilter))):
ext = re.search("\*(\.[a-z]{1,10})", selectedFileFilter)
if ext:
fileName += ext.group(1)
settings.setValue('/Processing/LastOutputPath',
os.path.dirname(fileName))
settings.setValue('/Processing/encoding', encoding)

uri = QgsDataSourceURI()
uri.setDatabase(fileName)
uri.setDataSource('', self.output.name.lower(), 'the_geom')
self.leText.setText("spatialite:" + uri.uri())

def saveToMemory(self):
self.leText.setText('memory:')

Expand Down Expand Up @@ -167,6 +207,8 @@ def getValue(self):
value = fileName
elif fileName.startswith('postgis:'):
value = fileName
elif fileName.startswith('spatialite:'):
value = fileName
elif not os.path.isabs(fileName):
value = ProcessingConfig.getSetting(
ProcessingConfig.OUTPUT_FOLDER) + os.sep + fileName
Expand Down
41 changes: 40 additions & 1 deletion python/plugins/processing/tools/vector.py
Expand Up @@ -17,6 +17,7 @@
***************************************************************************
"""
from processing.algs.qgis import postgis_utils
from processing.algs.qgis import spatialite_utils

__author__ = 'Victor Olaya'
__date__ = 'February 2013'
Expand Down Expand Up @@ -69,6 +70,13 @@
QVariant.Bool: "BOOLEAN"
}

TYPE_MAP_SPATIALITE_LAYER = {
QVariant.String: "VARCHAR",
QVariant.Double: "REAL",
QVariant.Int: "INTEGER",
QVariant.Bool: "INTEGER"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IS this really like that?? or is it a typo? I am no expert in spatialite...so just asking, since it looks a bit confusing to me at first :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Boolean does not exist in SQLite.
In the documentation http://www.sqlite.org/datatype3.html you'll find that Boolean values are stored as integers 0 (false) and 1 (true).

}


def features(layer):
"""This returns an iterator over features in a vector layer,
Expand Down Expand Up @@ -423,6 +431,7 @@ class VectorWriter:

MEMORY_LAYER_PREFIX = 'memory:'
POSTGIS_LAYER_PREFIX = 'postgis:'
SPATIALITE_LAYER_PREFIX = 'spatialite:'

def __init__(self, destination, encoding, fields, geometryType,
crs, options=None):
Expand Down Expand Up @@ -485,7 +494,37 @@ def _runSQL(sql):
table=uri.table().lower(), schema=uri.schema(), srid=crs.authid().split(":")[-1],
typmod=GEOM_TYPE_MAP[geometryType].upper()))

self.layer = QgsVectorLayer(uri.uri(), uri.table(), "postgres")
self.layer = QgsVectorLayer(uri.uri(), uri.table(), "spatialite")
self.writer = self.layer.dataProvider()
elif self.destination.startswith(self.SPATIALITE_LAYER_PREFIX):
self.isNotFileBased = True
uri = QgsDataSourceURI(self.destination[len(self.SPATIALITE_LAYER_PREFIX):])
print uri.uri()
try:
db = spatialite_utils.GeoDB(uri=uri)
except spatialite_utils.DbError as e:
raise GeoAlgorithmExecutionException(
"Couldn't connect to database:\n%s" % e.message)

def _runSQL(sql):
try:
db._exec_sql_and_commit(unicode(sql))
except spatialite_utils.DbError as e:
raise GeoAlgorithmExecutionException(
'Error creating output Spatialite table:\n%s' % e.message)

fields = [_toQgsField(f) for f in fields]
fieldsdesc = ",".join('%s %s' % (f.name(),
TYPE_MAP_SPATIALITE_LAYER.get(f.type(), "VARCHAR"))
for f in fields)

_runSQL("DROP TABLE IF EXISTS %s" % uri.table().lower())
_runSQL("CREATE TABLE %s (%s)" % (uri.table().lower(), fieldsdesc))
_runSQL("SELECT AddGeometryColumn('{table}', 'the_geom', {srid}, '{typmod}', 2)".format(
table=uri.table().lower(), srid=crs.authid().split(":")[-1],
typmod=GEOM_TYPE_MAP[geometryType].upper()))

self.layer = QgsVectorLayer(uri.uri(), uri.table(), "spatialite")
self.writer = self.layer.dataProvider()
else:
formats = QgsVectorFileWriter.supportedFiltersAndFormats()
Expand Down