This plugin reads and writes QGIS map projects, including data, style and related resources from/into a geopackage file.
\n"
58 | "
\n"
59 | "
It supports the qgis and ows geopackage extensions. The qgis geopackage extension was created with the goal of enabling QGIS users to share their projects, while the ows geopackage extension was designed for interoperability, enabling porting map projects between different mapping frameworks; on the former the approach is to store a QGIS project file in an sqlite table, while on the latter the project is encoded using OGC OWS context standard, on a different sqlite table
\n"
60 | "
\n"
61 | "
Currently, writing is only supported using the qgis geopackage extension.
\n"
62 | "
\n"
63 | "
Authors: Cedric Christen, Pirmin Kalberer (pka@sourcepole.ch), Joana Simoes (joana.simoes@geocat.net), Paul van Genuchten
\n"
64 | "
from SourcePole and GeoCat
", None))
65 |
66 |
--------------------------------------------------------------------------------
/qgis_plugin/qgpkg/ui_about_dialog.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | qgpkgDlg
4 |
5 |
6 |
7 | 0
8 | 0
9 | 456
10 | 358
11 |
12 |
13 |
14 | QGIS map project GeoPackage extension
15 |
16 |
17 |
18 | :/plugins/QgisGeopackage/about.png:/plugins/QgisGeopackage/about.png
19 |
20 |
21 |
22 |
23 |
24 | true
25 |
26 |
27 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
28 | <html><head><meta name="qrichtext" content="1" /><style type="text/css">
29 | p, li { white-space: pre-wrap; }
30 | </style></head><body style=" font-family:'Noto Sans'; font-size:10pt; font-weight:400; font-style:normal;">
31 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu'; font-size:9pt; font-weight:600;">ABOUT</span></p>
32 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Ubuntu'; font-size:9pt;"><br /></p>
33 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu'; font-size:9pt;">This plugin reads and writes QGIS map projects, including data, style and related resources from/into a geopackage file.</span></p>
34 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>
35 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu'; font-size:9pt;">It supports the </span><a href="https://github.com/pka/qgpkg/blob/master/qgis_geopackage_extension.md"><span style=" text-decoration: underline; color:#2980b9;">qgis</span></a><span style=" font-family:'Ubuntu'; font-size:9pt;"> and </span><a href="https://github.com/pka/qgpkg/blob/master/ows_geopackage_extension.md"><span style=" text-decoration: underline; color:#2980b9;">ows</span></a><span style=" font-family:'Ubuntu'; font-size:9pt;"> geopackage extensions. The </span><span style=" font-family:'Ubuntu'; font-size:9pt; font-weight:600;">qgis geopackage extension</span><span style=" font-family:'Ubuntu'; font-size:9pt;"> was created with the goal of enabling QGIS users to share their projects, while the </span><span style=" font-family:'Ubuntu'; font-size:9pt; font-weight:600;">ows geopackage extension</span><span style=" font-family:'Ubuntu'; font-size:9pt;"> was designed for interoperability, enabling porting map projects between different mapping frameworks; on the former the approach is to store a QGIS project file in an sqlite table, while on the latter the project is encoded using OGC OWS context standard, on a different sqlite table</span></p>
36 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Ubuntu'; font-size:9pt;"><br /></p>
37 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu'; font-size:9pt;">Currently, writing is only supported using the qgis geopackage extension.</span></p>
38 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Ubuntu'; font-size:9pt;"><br /></p>
39 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu'; font-size:9pt;">Authors: Cedric Christen, Pirmin Kalberer (pka@sourcepole.ch), Joana Simoes (joana.simoes@geocat.net), Paul van Genuchten</span></p>
40 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu'; font-size:9pt;">from SourcePole and GeoCat</span></p></body></html>
41 |
42 |
43 |
44 |
45 |
46 |
47 | Qt::Horizontal
48 |
49 |
50 | QDialogButtonBox::Close
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | button_box
60 | accepted()
61 | qgpkgDlg
62 | accept()
63 |
64 |
65 | 20
66 | 20
67 |
68 |
69 | 20
70 | 20
71 |
72 |
73 |
74 |
75 | button_box
76 | rejected()
77 | qgpkgDlg
78 | reject()
79 |
80 |
81 | 20
82 | 20
83 |
84 |
85 | 20
86 | 20
87 |
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/qgis_plugin/qgpkg/write.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pka/qgpkg/eb5e3c309ff97048799ce97b9d6499edde977a20/qgis_plugin/qgpkg/write.png
--------------------------------------------------------------------------------
/qgisgpkg/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pka/qgpkg/eb5e3c309ff97048799ce97b9d6499edde977a20/qgisgpkg/__init__.py
--------------------------------------------------------------------------------
/qgisgpkg/qgpkg.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE in the QGIS plugin directory.
2 | # Edit the original library file in the qgpkg directory and
3 | # execute `make` to update the QGIS plugin files.
4 | import os
5 | import sqlite3
6 | import logging
7 |
8 | logger = logging.getLogger('qgpkg')
9 |
10 | class QGpkg:
11 | """Base class for handling data and style information within a GeoPackage database file"""
12 |
13 | def read(self, gpkg_path): pass
14 | """Abstract method to read a QGIS project from geopackage.
15 |
16 | Args:
17 | gpkg_path: The geopackage path on disk.
18 | """
19 |
20 | def write(self, project_path): pass
21 | """Abstract method to write a QGIS project into a geopackage.
22 |
23 | Args:
24 | gpkg_path: The geopackage path on disk.
25 | """
26 |
27 | def __init__(self, gpkg, logfunc):
28 | self._gpkg = gpkg
29 | self._log = logfunc
30 |
31 | def log(self, lvl, msg, *args, **kwargs):
32 | self._log(lvl, msg, *args, **kwargs)
33 |
34 | def _connect_read_only(self):
35 | ''' Connect database with sqlite3 '''
36 | try:
37 | conn = sqlite3.connect(self._gpkg)
38 | # Open in read-only mode needs Python 3.4+
39 | # conn = sqlite3.connect('file:%s?mode=ro' % self._gpkg, uri=True)
40 | # Workaround:
41 | if os.stat(self._gpkg).st_size == 0:
42 | os.remove(self._gpkg)
43 | self.log(logging.ERROR,
44 | "Couldn't find GeoPackage '%s'" % self._gpkg)
45 | return None
46 | conn.row_factory = sqlite3.Row
47 | return conn.cursor()
48 | except sqlite3.Error as e:
49 | self.log(logging.ERROR,
50 | "Couldn't connect to GeoPackage: %s" % e.args[0])
51 | return None
52 |
53 | def info(self):
54 | ''' Show information about GeoPackage '''
55 | cur = self._connect_read_only()
56 | if not cur:
57 | return
58 | data_type = None
59 | try:
60 | for row in cur.execute('''SELECT * FROM gpkg_contents
61 | ORDER BY data_type'''):
62 | if row['data_type'] != data_type:
63 | data_type = row['data_type']
64 | print("gpkg_contents %s:" % data_type)
65 | print(row['table_name'])
66 | except sqlite3.Error as e:
67 | self.log(logging.ERROR, "GeoPackage access error: ", e.args[0])
68 |
69 | try:
70 | rows = list(cur.execute('''SELECT extension_name FROM gpkg_extensions'''))
71 | if len(rows) > 0:
72 | print("GPKG extensions:")
73 | for row in rows:
74 | print(row['extension_name'])
75 | except sqlite3.Error:
76 | pass
77 |
78 | try:
79 | rows = list(cur.execute('''SELECT name FROM qgis_projects'''))
80 | if len(rows) > 0:
81 | print("QGIS projects:")
82 | for row in rows:
83 | print(row['name'])
84 | except sqlite3.Error:
85 | pass
86 |
87 | try:
88 | rows = list(cur.execute('''SELECT name, mime_type FROM qgis_resources'''))
89 | if len(rows) > 0:
90 | print("QGIS recources:")
91 | for row in rows:
92 | print(row['name'] + ' (%s)' % row['mime_type'])
93 | except sqlite3.Error:
94 | pass
95 |
96 | def database_connect(self, path):
97 | ''' Connect database with sqlite3 '''
98 | try:
99 | self.conn = sqlite3.connect(path)
100 | self.c = self.conn.cursor()
101 | return True
102 | except sqlite3.Error as e:
103 | self.log(logging.ERROR,
104 | "Couldn't connect to GeoPackage: %s" % e.args[0])
105 | return False
106 |
107 | def check_gpkg(self, path):
108 | ''' Check if file is GeoPackage '''
109 | try:
110 | self.c.execute('SELECT * FROM gpkg_contents')
111 | self.c.fetchone()
112 | return True
113 | except:
114 | return False
115 |
116 | def make_path_absolute(self, path, project_path):
117 | ''' Make path absolut and handle multiplatform issues '''
118 | if not os.path.isabs(path):
119 | path = os.path.join(os.path.dirname(project_path), path)
120 | return os.path.normpath(path)
121 |
--------------------------------------------------------------------------------
/qgisgpkg/qgpkg_owc.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE in the QGIS plugin directory.
2 | # Edit the original library file in the qgpkg directory and
3 | # execute `make` to update the QGIS plugin files.
4 | from __future__ import print_function
5 | import sys
6 | import os
7 | import sqlite3
8 | import tempfile
9 | import logging
10 | from xml.etree import ElementTree as ET
11 | from qgis.core import *
12 | from qgis.utils import *
13 | from PyQt4.QtXml import *
14 | from PyQt4.QtCore import *
15 | from StringIO import StringIO
16 | from urlparse import urlparse
17 | import sys
18 |
19 | from qgpkg import QGpkg
20 |
21 | logger = logging.getLogger('qgpkg')
22 |
23 | # Debug code for Pycharm
24 | #sys.path.append('/home/joana/Downloads/pycharm-2016.3.3/debug-eggs/pycharm-debug.egg')
25 | #import pydevd
26 |
27 | #pydevd.settrace('localhost', port=53100, stdoutToServer=True, stderrToServer=True)
28 |
29 |
30 | class QGpkg_owc (QGpkg):
31 | """Read and write QGIS mapping information in a GeoPackage database file, using this spec:
32 | https://github.com/pka/qgpkg/blob/master/ows_geopackage_extension.md
33 | """
34 |
35 | def write(self, project_path):
36 | self.log(logging.ERROR, u"Sorry, but it appears that writing into this geopackage extension was not implemented yet!")
37 | return
38 |
39 | def read(self, gpkg_path):
40 |
41 | iface.newProject(True) # Clear project, before opening
42 |
43 | ''' Read QGIS project from GeoPackage '''
44 | # Check if it's a GeoPackage Database
45 | self.database_connect(gpkg_path)
46 | if not self.check_gpkg(gpkg_path):
47 | self.log(logging.ERROR, u"No valid GeoPackage selected.")
48 | return
49 |
50 | try:
51 | self.c.execute('SELECT table_name FROM gpkg_contents')
52 | except sqlite3.OperationalError:
53 | self.log(logging.ERROR, u"Unable to read table Name.")
54 | return
55 |
56 | table_names = self.c.fetchall()
57 |
58 | db_name = QFileInfo(gpkg_path).baseName()
59 |
60 | # Load OWS Context
61 | try:
62 | self.c.execute('SELECT content FROM owc_context')
63 | except sqlite3.OperationalError:
64 | self.log(logging.ERROR, u"Unable to read table owc_context.")
65 | return
66 |
67 | context = self.c.fetchone()
68 |
69 | if context is None:
70 | self.log(logging.ERROR, u"No record found on table owc_context!")
71 | return
72 |
73 | # Everything is read from the context
74 | self.loadContext(context[0], gpkg_path)
75 |
76 | # TODO: read resources
77 |
78 | def loadContext(self, context, gpkg_path):
79 | """Parses and applies the information on OWC_context.
80 |
81 | Args:
82 | context: The contents of owc_context table.
83 | gpkg_path: The path of the gpkg file.
84 | """
85 | it = ET.iterparse(StringIO(context))
86 | for _, el in it:
87 | if '}' in el.tag:
88 | el.tag = el.tag.split('}', 1)[1] # strip all namespaces
89 | root = it.root
90 |
91 | # Missing mandatory elements
92 | # spec reference
93 | spec_elem = root.find("specReference")
94 | # if spec_elem is None:
95 | # self.log(logging.ERROR, u"Could not parse project spec feference.")
96 | # return
97 | # Language
98 | lang_elem = root.find("language")
99 | # if lang_elem is None:
100 | # self.log(logging.ERROR, u"Could not parse project language.")
101 | # return
102 |
103 | # Parse project id (mandatory)
104 | id_elem = root.find("id")
105 | if id_elem is None:
106 | self.log(logging.ERROR, u"Could not parse project id.")
107 | return
108 |
109 | # Parse project title (mandatory)
110 | title_elem = root.find("title")
111 | if title_elem is None:
112 | self.log(logging.ERROR, u"Could not parse project title.")
113 | return
114 |
115 | QgsProject.instance().setTitle(title_elem.text)
116 |
117 | # Parse bbox, if it exists (not owc)
118 | where_elem = root.find("where")
119 | if where_elem is not None:
120 | self.loadBBbox(where_elem)
121 |
122 | # OWC (optional) elements
123 | # Parse abstract (optional)
124 | abstract_elem = root.find("abstract")
125 |
126 | # Parse update date (updateDate?) (optional)
127 | update_elem = root.find("update")
128 |
129 | # Parse author (optional)
130 | author_elem = root.find("author")
131 | # TODO: parse comma separated list
132 |
133 | # Parse publisher (optional)
134 | publisher_elem = root.find("publisher")
135 |
136 | # Parse creator (optional)
137 | creator_elem = root.find("creator")
138 |
139 | # Parse rights (optional)
140 | rights_elem = root.find("rights")
141 |
142 | # Parse area of interest (optional)
143 | aio_elem = root.find("areaOfInterest")
144 | # TODO: parse GM_Envelope
145 |
146 | # Parse time interval of interest (optional)
147 | time_elem = root.find("timeIntervalOfInterest")
148 |
149 | # Parse keyword (optional)
150 | keyword_elem = root.find("keyword")
151 |
152 | # Parse context metadata (optional)
153 | metadata_elem = root.find("contextMetadata")
154 |
155 | entry_elems = root.findall("entry") # owc:resource?
156 | if entry_elems is not None:
157 |
158 | entry_elems.reverse()
159 | # Load every entry
160 | for entry_elem in entry_elems:
161 | self.loadOWCLayer(gpkg_path, entry_elem)
162 |
163 | def loadOWCLayer(self, gpkg_path, entry_elem):
164 | """Parses layer information from an entry, on OWC_context, and uses it to load and style the layer.
165 |
166 | Args:
167 | gpkg_path: The geopackage path.
168 | entry_elem: The entry xml node.
169 | """
170 |
171 | # Id: is it called code?
172 | id_elem = entry_elem.find("id")
173 | if id_elem is None:
174 | self.log(logging.ERROR, u"Could not parse layer uri.")
175 | return
176 |
177 | # Parse RFC and check if there is a valid schema
178 | parsed_url = urlparse(id_elem.text)
179 | if (parsed_url.scheme) is None:
180 | self.log(logging.ERROR, u"Invalid layer uri.")
181 | return
182 |
183 | # Mandatory
184 | title_elem = entry_elem.find("title")
185 | if title_elem is None:
186 | self.log(logging.ERROR, u"Could not parse layer title.")
187 | return
188 |
189 | # TODO: make offering more general, to support other types of data formats (e.g.: wms)
190 | offering_elem = entry_elem.find("offering")
191 | if offering_elem is not None:
192 | # Mandatory
193 | content_elem = offering_elem.find("content")
194 | if content_elem is None:
195 | self.log(logging.ERROR, u"Failed to content '" + name + "' layer!")
196 | return;
197 | href = content_elem.get("href")
198 | name = self.find_between(href, "#table=")
199 | if name is None:
200 | self._log(logging.ERROR, u"Could not parse table name.")
201 | return
202 |
203 | layer = self.loadLayer(gpkg_path, name, title_elem.text)
204 | if layer is None or not layer.isValid():
205 | self.log(logging.ERROR, u"Layer '" + name + "' failed to load!")
206 | return;
207 |
208 | # layer.setShortName(name)
209 |
210 | # Check visibility (mandatory)
211 | visibility = entry_elem.find("category").get("term")
212 | if visibility is None:
213 | self.log(logging.ERROR, u"Failed to read visibility for '" + name + "' layer!")
214 | return;
215 |
216 | iface.legendInterface().setLayerVisible(layer, visibility.lower() == 'true')
217 |
218 | # Read style (optional)
219 | style_elem = offering_elem.find("styleSet")
220 | if style_elem is not None:
221 | self.loadOWCStyle(style_elem, title_elem.text)
222 |
223 | # Read other OWC (optional) elements ##################
224 |
225 | # Parse abstract (optional)
226 | abstract_elem = entry_elem.find("abstract")
227 | if abstract_elem is not None:
228 | layer.setAbstract(abstract_elem.text)
229 |
230 | # Parse update date (optional): shouldn't this be OWC:updateDate ?
231 | date_elem = entry_elem.find("updated")
232 |
233 | # Parse author (optional)
234 | author_elem = entry_elem.find("author")
235 | if author_elem is not None:
236 | layer.setAttribution(author_elem.text)
237 |
238 | # Parse publisher (optional)
239 | publisher_elem = entry_elem.find("publisher")
240 |
241 | # Parse rights (optional)
242 | rights_elem = entry_elem.find("rights")
243 |
244 | # Parse geospatial extent (optional)
245 | ext_elem = entry_elem.find("geospatialExtent")
246 | # TODO: parse GM_envelope
247 |
248 | # Parse temporal extent (optional)
249 | temp_elem = entry_elem.find("temporalExtent")
250 | # TODO: parse TM_GeometricPrimitive
251 |
252 | # Parse content description (optional)
253 | desc_elem = entry_elem.find("contentDescription")
254 |
255 | # Parse preview (optional)
256 | prev_elem = entry_elem.find("preview")
257 | # TODO: validate uri
258 |
259 | # Parse content reference (optional)
260 | ref_elem = entry_elem.find("contentByRef")
261 | # TODO: validate uri
262 |
263 | # Parse status (optional)
264 | active_elem = entry_elem.find("active")
265 | # TODO: validate boolean
266 |
267 | # Parse keyword (optional)
268 | keyword_elem = entry_elem.find("keyword")
269 | if keyword_elem is not None:
270 | keywords = [keyword_elem.text]
271 | layer.setKeywordList(keywords)
272 |
273 | # Parse minimum scale denominator (optional)
274 | minScale_elem = entry_elem.find("minScaleDenominator")
275 | if minScale_elem is not None:
276 | layer.setMinimumScale(float(minScale_elem.text))
277 |
278 | # Parse maximum scale denominator (optional)
279 | maxScale_elem = entry_elem.find("maxScaleDenominator")
280 | if maxScale_elem is not None:
281 | layer.setMaximumScale(float(maxScale_elem.text))
282 |
283 | # Parse resource metadata (optional)
284 | metadata_elem = entry_elem.find("resourceMetadata")
285 |
286 | # Parse folder (optional)
287 | folder_elem = entry_elem.find("folder")
288 |
289 | def loadOWCStyle(self, style_elem, layer_title):
290 | """Parses and applies style information from a styleSet, on OWC_context.
291 |
292 | Args:
293 | style_elem: The styleSet xml node.
294 | layer_title: The title of the layer to which we want to apply the style.
295 | """
296 |
297 | # Mandatory: given name
298 | stylename_elem = style_elem.find("name")
299 | if stylename_elem is None:
300 | self.log(logging.ERROR, u"Could not parse style name.")
301 | return
302 |
303 | # parse title (optional)
304 | title_elem = style_elem.find("title")
305 |
306 | # parse content (mandatory)
307 | href = style_elem.find("content").get("href")
308 | pref1 = "#table="
309 | pref2 = "&name="
310 | style_table = self.find_between(href, pref1, pref2)
311 |
312 | stylename = self.find_between(href, pref2)
313 | if stylename is None or stylename != stylename_elem.text:
314 | self._log(logging.ERROR, u"Could not parse style name.")
315 | return
316 |
317 | type = style_elem.find("content").get("type")
318 | if type != "application/sld+xml":
319 | self._log(logging.ERROR, u"Currently we only support styles in sld/xml format.")
320 | return
321 |
322 | self.loadStyle(stylename, style_table, layer_title)
323 |
324 | # Load layers from gpkg
325 | def loadLayer(self, gpkg_path, layername, title):
326 | """Loads a layer from a geopackage, and it sets its title.
327 |
328 | Args:
329 | gpkg_path: The gpkg path.
330 | layername: The layer name, within the geopackage.
331 | title: The title to be given to the layer.
332 |
333 | Returns:
334 | An handle to the loaded layer.
335 | """
336 | return iface.addVectorLayer(gpkg_path + "|layername=" + layername, title, "ogr")
337 |
338 | def loadStyle(self, style_name, table_name, given_name):
339 | """Load named style from a table.
340 |
341 | Args:
342 | style_name: The style name, as it is referenced in the style table.
343 | table_name: The name of the table where the style is stored (shouldn't it be a convention?).
344 | given_name: The layer title.
345 | """
346 | try:
347 | self.c.execute("SELECT content FROM owc_style where name like'" + style_name + "'")
348 | except sqlite3.OperationalError:
349 | self.log(logging.ERROR, u"Could not find style "
350 | + style_name)
351 | return
352 | styles = self.c.fetchone()
353 |
354 | if styles is None:
355 | self.log(logging.ERROR, u"Could not find any styles "
356 | u"named " + style_name)
357 | return
358 |
359 | style = styles[0]
360 |
361 | layerList = QgsMapLayerRegistry.instance().mapLayersByName(given_name)
362 |
363 | if layerList is None:
364 | self.log(logging.ERROR, u"We could not find a loaded layer "
365 | "called " + given_name + ". Something is not right!")
366 |
367 | layer = layerList[0]
368 |
369 | f = QTemporaryFile()
370 | if f.open():
371 | f.write(style)
372 | f.close()
373 | ret = layer.loadSldStyle(f.fileName())
374 | # TODO: add style to default styles?
375 |
376 | if ret[1] is True:
377 | self.log(logging.DEBUG, "Style '" + style_name + "' loaded")
378 | else:
379 | self.log(logging.ERROR, "Style '" + style_name + "' not loaded: " + ret[0])
380 |
381 | f.remove()
382 |
383 | else:
384 | self.log(logging.ERROR, u"Although there was a reference to style "
385 | + style_name + ", we could not find it in table owc_style. Something is not right!")
386 | return
387 |
388 | def loadBBbox(self, where_elem):
389 | """Parses and applies bbox.
390 |
391 | Args:
392 | where_elem: The where xml node.
393 | """
394 | env_elem = where_elem.find("Envelope")
395 | if env_elem is None:
396 | self.log(logging.ERROR, u"Could not parse envelope.")
397 | return
398 |
399 | lower_elem = env_elem.find("lowerCorner")
400 | if lower_elem is None:
401 | self.log(logging.ERROR, u"Could not parse lower corner.")
402 | return
403 | lc = lower_elem.text.split()
404 | if (len(lc) != 2):
405 | self.log(logging.ERROR, u"Wrong number of entries in lower corner.")
406 | return
407 |
408 | upper_elem = env_elem.find("upperCorner")
409 | if upper_elem is None:
410 | self.log(logging.ERROR, u"Could not parse lower corner.")
411 | return
412 | uc = upper_elem.text.split()
413 | if (len(uc) != 2):
414 | self.log(logging.ERROR, u"Wrong number of entries in upper corner.")
415 | return
416 |
417 | # TODO: review this implementation
418 | # str = (self.find_between(context, "", "")).replace('\n', '').encode('ascii',
419 | # 'ignore')
420 | # d = QDomDocument()
421 | # d.setContent("< ?xml version = \"1.0\" encoding = \"utf-8\"? >" + str.)
422 | # docElem = d.documentElement()
423 | # extent = QgsOgcUtils.rectangleFromGMLEnvelope(docElem.firstChild())
424 |
425 | # TODO: what happens to srs and dimension?
426 | extent = QgsRectangle(float(lc[0]), float(lc[1]), float(uc[0]), float(uc[1]))
427 |
428 | iface.mapCanvas().setExtent(extent)
429 | iface.mapCanvas().refresh()
430 |
431 | def find_between(self, s, first, last=None):
432 | """Extracts a substring from a string, between one, or two substrings.
433 | If the last parameter is empty, it will extract everything after the first substring.
434 |
435 | Args:
436 | s: The string we want to parse.
437 | first: The first substring.
438 | last: The last substring (optional).
439 |
440 | Returns:
441 | The extracted substring.
442 | """
443 | try:
444 | start = s.index(first) + len(first)
445 | if last is None:
446 | end = len(s)
447 | else:
448 | end = s.index(last, start)
449 |
450 | return s[start:end]
451 |
452 | except ValueError:
453 | return ""
454 |
--------------------------------------------------------------------------------
/qgisgpkg/qgpkg_qgis.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE in the QGIS plugin directory.
2 | # Edit the original library file in the qgpkg directory and
3 | # execute `make` to update the QGIS plugin files.
4 | from __future__ import print_function
5 | import os
6 | import sqlite3
7 | import tempfile
8 | import mimetypes
9 | import logging
10 |
11 | from xml.etree import ElementTree as ET
12 |
13 | from qgpkg import QGpkg
14 |
15 | logger = logging.getLogger('qgpkg_qgis')
16 |
17 | # Debug code for Pycharm
18 | #sys.path.append('/home/joana/Downloads/pycharm-2016.3.3/debug-eggs/pycharm-debug.egg')
19 | #import pydevd
20 |
21 | #pydevd.settrace('localhost', port=53100, stdoutToServer=True, stderrToServer=True)
22 |
23 | class QGpkg_qgis(QGpkg):
24 | """Read and write QGIS mapping information in a GeoPackage database file, using this spec:
25 | https://github.com/pka/qgpkg/blob/master/qgis_geopackage_extension.md
26 | """
27 |
28 | def write(self, project_path):
29 | ''' Store QGIS project '''
30 | xmltree = self.read_project(project_path)
31 | # If something is messed up with the file, the Method will stop
32 | if not xmltree:
33 | self.log(logging.ERROR, u"Couldn't read project (wrong file format)")
34 | return None
35 |
36 | root = xmltree.getroot()
37 | projectlayers = root.find("projectlayers")
38 |
39 | # Search for layersources
40 | sources = []
41 | for layer in projectlayers:
42 | layer_path = self.make_path_absolute(layer.find(
43 | "datasource").text.split("|")[0], project_path)
44 | if layer_path not in sources:
45 | self.log(logging.DEBUG, u"Found datasource: %s" % layer_path)
46 | sources.append(layer_path)
47 |
48 | # If there are more than just one different datasource check from where
49 | # they are from
50 | gpkg_found = False
51 | if len(sources) >= 1:
52 | for path in sources:
53 | if self.database_connect(path):
54 | if self.check_gpkg(path) and not gpkg_found:
55 | gpkg_found = True
56 | gpkg_path = path
57 | elif self.check_gpkg(path) and gpkg_found:
58 | # If a project has layer from more than just one
59 | # GeoPackage it can't be written
60 | self.log(logging.ERROR, u"The project uses layers "
61 | "from different GeoPackage databases.")
62 | return None
63 | if gpkg_found and len(sources) > 1:
64 | self.log(
65 | logging.WARNING,
66 | u"Some layers aren't in the GeoPackage. It can't be "
67 | "garanteed that all layers will be shown properly.")
68 |
69 | if not gpkg_found:
70 | self.log(logging.ERROR, u"There is no GeoPackage layer "
71 | "in the project.")
72 | return None
73 |
74 | # Check for images in the composer of the project
75 | images = []
76 | for composer in root.findall("Composer"):
77 | for comp in composer:
78 | for composer_picture in comp.findall("ComposerPicture"):
79 | img = composer_picture.attrib['file']
80 | if img not in images:
81 | self.log(logging.DEBUG, u"Image found: %s" % img)
82 | images.append(img)
83 |
84 | # Write data in database
85 | project_name = os.path.basename(project_path)
86 | project_xml = ET.tostring(root)
87 |
88 | self.database_connect(gpkg_path)
89 |
90 | # Create tables
91 | self.c.execute('CREATE TABLE IF NOT EXISTS qgis_projects (name TEXT PRIMARY KEY, xml TEXT NOT NULL)')
92 | self.c.execute(
93 | """CREATE TABLE IF NOT EXISTS qgis_resources
94 | (name TEXT PRIMARY KEY, mime_type TEXT NOT NULL, content BLOB NOT NULL)""")
95 | self.c.execute(
96 | 'CREATE TABLE IF NOT EXISTS gpkg_extensions (table_name TEXT,column_name TEXT,extension_name TEXT NOT NULL,definition TEXT NOT NULL,scope TEXT NOT NULL,CONSTRAINT ge_tce UNIQUE (table_name, column_name, extension_name))')
97 | extension_record = (None, None, 'qgis',
98 | 'http://github.com/pka/qgpkg/blob/master/'
99 | 'qgis_geopackage_extension.md',
100 | 'read-write')
101 | self.c.execute('SELECT count(1) FROM gpkg_extensions WHERE extension_name=?', (extension_record[2],))
102 | if self.c.fetchone()[0] == 0:
103 | self.c.execute(
104 | 'INSERT INTO gpkg_extensions VALUES (?,?,?,?,?)', extension_record)
105 |
106 | self.c.execute('SELECT count(1) FROM qgis_projects WHERE name=?', (project_name,))
107 | if self.c.fetchone()[0] == 0:
108 | self.c.execute('INSERT INTO qgis_projects VALUES (?,?)', (project_name, project_xml))
109 | self.log(logging.DEBUG, u"Project %s saved." % project_name)
110 | else:
111 | # Overwrite existing project (DELETE gives locking problems)
112 | self.c.execute('UPDATE qgis_projects SET xml=? WHERE name=?',
113 | (project_xml, project_name))
114 | self.log(logging.INFO, u"Project overwritten.")
115 |
116 | if images:
117 | for image in images:
118 | img = self.make_path_absolute(image, project_path)
119 | with open(img, 'rb') as input_file:
120 | blob = input_file.read()
121 | mime_type = mimetypes.MimeTypes().guess_type(image)[0]
122 | self.c.execute('SELECT count(1) FROM qgis_resources WHERE name=?', (image,))
123 | if self.c.fetchone()[0] == 0:
124 | self.conn.execute(
125 | """INSERT INTO qgis_resources \
126 | VALUES(?, ?, ?)""", (image, mime_type, sqlite3.Binary(blob)))
127 | self.log(logging.DEBUG, u"Image %s was saved" % image)
128 | else:
129 | # TODO: forced overwrite
130 | self.log(logging.DEBUG, u"Skipping existing image %s" % image)
131 | self.conn.commit()
132 |
133 | return gpkg_path
134 |
135 | def read(self, gpkg_path):
136 | ''' Read QGIS project from GeoPackage '''
137 | # Check if it's a GeoPackage Database
138 | self.database_connect(gpkg_path)
139 | if not self.check_gpkg(gpkg_path):
140 | self.log(logging.ERROR, u"No valid GeoPackage selected.")
141 | return None
142 |
143 | # Read xml from the project in the Database
144 | try:
145 | self.c.execute('SELECT name, xml FROM qgis_projects')
146 | except sqlite3.OperationalError:
147 | self.log(logging.ERROR, u"There is no Project file "
148 | "in the database.")
149 | return None
150 | file_name, xml = self.c.fetchone()
151 | try:
152 | xml_tree = ET.ElementTree()
153 | root = ET.fromstring(xml)
154 | except:
155 | self.log(logging.ERROR, u"The xml code is corrupted.")
156 | return None
157 | self.log(logging.DEBUG, u"Xml successfully read.")
158 | xml_tree._setroot(root)
159 | projectlayers = root.find("projectlayers")
160 |
161 | # Layerpath in xml adjusted
162 | tmp_folder = tempfile.mkdtemp()
163 | project_path = os.path.join(tmp_folder, file_name)
164 | for layer in projectlayers:
165 | layer_element = layer.find("datasource")
166 | layer_info = layer_element.text.split("|")
167 | layer_path = self.make_path_absolute(gpkg_path, layer_info[0])
168 | if layer_path.endswith('.gpkg'):
169 | if len(layer_info) >= 2:
170 | for i in range(len(layer_info)):
171 | if i == 0:
172 | layer_element.text = layer_path
173 | else:
174 | layer_element.text += "|" + layer_info[i]
175 | elif len(layer_info) == 1:
176 | layer_element.text = layer_path
177 | self.log(logging.DEBUG,
178 | u"Layerpath from layer %s was adjusted." %
179 | layer.find("layername").text)
180 |
181 | # Check if an image is available
182 | images = []
183 | for composer in root.findall("Composer"):
184 | for comp in composer:
185 | for composer_picture in comp.findall("ComposerPicture"):
186 | img = self.make_path_absolute(
187 | composer_picture.attrib['file'], project_path)
188 | # If yes, the path will be adjusted
189 | composer_picture.set('file', './' + os.path.basename(img))
190 | self.log(logging.DEBUG,
191 | u"External image %s found." % os.path.basename(img))
192 | images.append(img)
193 |
194 | # and the image will be saved in the same folder as the project
195 | if images:
196 | self.c.execute("SELECT name, mime_type, content FROM qgis_resources")
197 | images = self.c.fetchall()
198 | for img in images:
199 | img_name, mime_type, blob = img
200 | img_path = os.path.join(tmp_folder, img_name)
201 | with open(img_path, 'wb') as file:
202 | file.write(blob)
203 | self.log(logging.DEBUG, u"Image saved: %s" % img_name)
204 |
205 | # Project is saved and started
206 | xml_tree.write(project_path)
207 | self.log(logging.DEBUG, u"Temporary project written: %s" % project_path)
208 | return project_path
209 |
210 | def read_project(self, path):
211 | ''' Check if it's a file and give ElementTree object back '''
212 | if not os.path.isfile(path):
213 | return False
214 |
215 | return ET.parse(path)
--------------------------------------------------------------------------------
/qgpkg_cli/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pka/qgpkg/eb5e3c309ff97048799ce97b9d6499edde977a20/qgpkg_cli/__init__.py
--------------------------------------------------------------------------------
/qgpkg_cli/qgpkg.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import argparse
4 | import sys
5 | import logging
6 | from qgisgpkg.qgpkg import QGpkg
7 | from qgisgpkg.qgpkg_qgis import QGpkg_qgis
8 |
9 | logger = logging.getLogger('qgpkg')
10 | logger.addHandler(logging.StreamHandler())
11 |
12 |
13 | def log(lvl, msg, *args, **kwargs):
14 | logger.log(lvl, msg, *args, **kwargs)
15 |
16 |
17 | def info(args):
18 | gpkg = QGpkg(args.gpkg, log)
19 | gpkg.info()
20 | return 0
21 |
22 |
23 | def write(args):
24 | gpkg = QGpkg_qgis(args.gpkg, log)
25 | gpkg.write(args.qgs)
26 | return 0
27 |
28 |
29 | def read(args):
30 | gpkg = QGpkg_qgis(args.gpkg, log)
31 | project_path = gpkg.read(args.gpkg)
32 | print "Project extracted: %s" % project_path
33 | return 0
34 |
35 |
36 | def main():
37 | """Returns 0 on success, 1 on error, for sys.exit."""
38 |
39 | parser = argparse.ArgumentParser(
40 | description="Store QGIS map information in GeoPackages")
41 |
42 | # Commands
43 | subparsers = parser.add_subparsers(title='commands',
44 | description='valid commands')
45 | # Common parameters
46 | gpkgparam = {
47 | 'help': "input datagpkg"
48 | }
49 | qgsparam = {
50 | 'nargs': '?',
51 | 'help': "output datagpkg",
52 | 'default': sys.stdout
53 | }
54 | parser.add_argument(
55 | '--debug', default=False, action='store_true',
56 | help='Display debugging information')
57 |
58 | subparser = subparsers.add_parser(
59 | 'info', help='GeoPackage content information')
60 | subparser.add_argument('gpkg', **gpkgparam)
61 | subparser.set_defaults(func=info)
62 |
63 | subparser = subparsers.add_parser(
64 | 'write', help='Save QGIS project in GeoPackage')
65 | subparser.add_argument('gpkg', **gpkgparam)
66 | subparser.add_argument('qgs', **qgsparam)
67 | subparser.set_defaults(func=write)
68 |
69 | subparser = subparsers.add_parser(
70 | 'read', help='Read QGIS project from GeoPackage')
71 | subparser.add_argument('gpkg', **gpkgparam)
72 | subparser.set_defaults(func=read)
73 |
74 | args = parser.parse_args()
75 |
76 | if args.debug:
77 | logger.setLevel(logging.DEBUG)
78 | else:
79 | logger.setLevel(logging.INFO)
80 |
81 | return args.func(args)
82 |
83 |
84 | if __name__ == '__main__':
85 | sys.exit(main())
86 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup(
4 | name='qgpkg',
5 | version='0.0.0',
6 | author='Pirmin Kalberer',
7 | author_email='pka@sourcepole.ch',
8 | packages=['qgisgpkg', 'qgpkg_cli'],
9 | url='https://github.com/pka/qgpkg',
10 | license='LICENSE.txt',
11 | description='Store QGIS map information in GeoPackages.',
12 | long_description=open('README.rst').read(),
13 | # tests_require=['nose'],
14 | # test_suite='nose.collector',
15 | classifiers=[
16 | 'Development Status :: 4 - Beta',
17 | 'Intended Audience :: Developers',
18 | 'Intended Audience :: Science/Research',
19 | 'License :: OSI Approved :: MIT License',
20 | 'Operating System :: OS Independent',
21 | 'Programming Language :: Python :: 2',
22 | 'Topic :: Scientific/Engineering :: GIS',
23 | ],
24 | entry_points={
25 | 'console_scripts': ['ogr = gpkg_cli.gpkg:main'],
26 | }
27 | )
28 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 |
4 | logger = logging.getLogger('qgpkg')
5 | logger.addHandler(logging.StreamHandler())
6 |
7 |
8 | def nolog(lvl, msg, *args, **kwargs):
9 | pass
10 |
--------------------------------------------------------------------------------
/tests/data/qgis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pka/qgpkg/eb5e3c309ff97048799ce97b9d6499edde977a20/tests/data/qgis.png
--------------------------------------------------------------------------------
/tests/data/small_world.gpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pka/qgpkg/eb5e3c309ff97048799ce97b9d6499edde977a20/tests/data/small_world.gpkg
--------------------------------------------------------------------------------
/tests/data/small_world.qgs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | degrees
18 |
19 | -263.08080742151162212
20 | -175.16116845015005765
21 | 263.08080742151162212
22 | 69.61429554114846496
23 |
24 | 0
25 | 0
26 |
27 |
28 | +proj=longlat +datum=WGS84 +no_defs
29 | 3452
30 | 4326
31 | EPSG:4326
32 | WGS 84
33 | longlat
34 | WGS84
35 | true
36 |
37 |
38 | 0
39 |
40 |
41 |
42 |
43 | small_world20160822231200722
44 | small_world20160822231206236
45 |
46 |
47 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 | -180
159 | -90
160 | 180
161 | 83.64509999999999934
162 |
163 | small_world20160822231200722
164 | ./small_world.gpkg
165 |
166 |
167 |
168 | small_world ne_110m_admin_0_countries Polygon
169 |
170 |
171 | +proj=longlat +datum=WGS84 +no_defs
172 | 3452
173 | 4326
174 | EPSG:4326
175 | WGS 84
176 | longlat
177 | WGS84
178 | true
179 |
180 |
181 | ogr
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 | 0
361 | 0
362 | 41
363 | name
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 | .
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 | .
426 |
427 | 0
428 | .
429 |
446 | 0
447 | generatedlayout
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 | -180
457 | -90
458 | 180
459 | 90
460 |
461 | small_world20160822231206236
462 | ./small_world.gpkg
463 |
464 |
465 |
466 | small_world
467 |
468 |
469 | +proj=longlat +datum=WGS84 +no_defs
470 | 3452
471 | 4326
472 | EPSG:4326
473 | WGS 84
474 | longlat
475 | WGS84
476 | true
477 |
478 |
479 |
480 |
481 |
482 | gdal
483 |
484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 |
499 |
500 | 0
501 |
502 |
503 |
504 |
505 | meters
506 | m2
507 |
508 |
509 | +proj=somerc +lat_0=46.95240555555556 +lon_0=7.439583333333333 +k_0=1 +x_0=600000 +y_0=200000 +ellps=bessel +towgs84=674.4,15.1,405.3,0,0,0,0 +units=m +no_defs
510 | EPSG:21781
511 | 1919
512 |
513 |
514 | false
515 |
516 |
517 | 0
518 | 255
519 | 255
520 | 255
521 | 255
522 | 255
523 | 255
524 |
525 |
526 | 2
527 | current_layer
528 | off
529 | 0
530 |
531 |
532 | 2
533 | true
534 |
535 |
538 |
539 |
540 |
541 |
--------------------------------------------------------------------------------
/tests/test_info.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from qgisgpkg.qgpkg import QGpkg
3 | from . import nolog
4 |
5 |
6 | def test_info():
7 | gpkg = QGpkg('tests/data/small_world.gpkg', nolog)
8 | gpkg.info()
9 | output = sys.stdout.getvalue().strip()
10 | info = """gpkg_contents features:
11 | ne_110m_admin_0_countries
12 | gpkg_contents tiles:
13 | small_world
14 | GPKG extensions:
15 | gpkg_rtree_index"""
16 | assert output == info
17 |
18 |
19 | def test_wrong_file():
20 | gpkg = QGpkg('wrong_file_name.gpkg', nolog)
21 | gpkg.info()
22 | output = sys.stdout.getvalue().strip()
23 | assert output == ""
24 |
25 |
26 | def test_no_gpkg():
27 | gpkg = QGpkg("./tests/test_info.py", nolog)
28 | gpkg.info()
29 | output = sys.stdout.getvalue().strip()
30 | assert output == ""
31 |
--------------------------------------------------------------------------------
/tests/test_readwrite.py:
--------------------------------------------------------------------------------
1 | from nose.tools import assert_equals
2 | import sys
3 | import tempfile
4 | import shutil
5 | import os
6 | from qgisgpkg.qgpkg_qgis import QGpkg_qgis
7 | import sqlite3
8 | from . import nolog
9 |
10 |
11 | def test_read_without_qgs():
12 | gpkg = QGpkg_qgis('tests/data/small_world.gpkg', nolog)
13 | gpkg.read('tests/data/small_world.gpkg')
14 | gpkg.info()
15 | output = sys.stdout.getvalue().strip()
16 | info = """gpkg_contents features:
17 | ne_110m_admin_0_countries
18 | gpkg_contents tiles:
19 | small_world
20 | GPKG extensions:
21 | gpkg_rtree_index"""
22 | assert output == info
23 |
24 |
25 | def copy_to_tmp(srcdir):
26 | tmp_folder = tempfile.mkdtemp()
27 | for fn in os.listdir(srcdir):
28 | shutil.copy(os.path.join(srcdir, fn), tmp_folder)
29 | return tmp_folder
30 |
31 |
32 | def test_write():
33 | # Copy test data
34 | tmp_folder = copy_to_tmp('tests/data')
35 | gpkg_path = os.path.join(tmp_folder, 'small_world.gpkg')
36 | qgs_path = os.path.join(tmp_folder, 'small_world.qgs')
37 | gpkg = QGpkg_qgis(gpkg_path, nolog)
38 | gpkg.write(qgs_path)
39 | gpkg.info()
40 | output = sys.stdout.getvalue().strip()
41 | info = """gpkg_contents features:
42 | ne_110m_admin_0_countries
43 | gpkg_contents tiles:
44 | small_world
45 | GPKG extensions:
46 | qgis
47 | gpkg_rtree_index
48 | QGIS projects:
49 | small_world.qgs
50 | QGIS recources:
51 | ./qgis.png (image/png)"""
52 | assert output == info
53 |
54 | conn = sqlite3.connect(gpkg_path)
55 | curs = conn.cursor()
56 |
57 | curs.execute('SELECT name FROM qgis_projects')
58 | assert_equals('small_world.qgs', curs.fetchone()[0], 'small_world.qgs not found')
59 | assert curs.fetchone() is None
60 |
61 | curs.execute('SELECT name FROM qgis_resources')
62 | assert_equals('./qgis.png', curs.fetchone()[0], 'Image not found')
63 | assert curs.fetchone() is None
64 |
65 | curs.execute("SELECT scope FROM gpkg_extensions WHERE extension_name = 'qgis'")
66 | assert_equals('read-write', curs.fetchone()[0], 'Extension registration missing')
67 | assert curs.fetchone() is None
68 |
69 | # Test overwriting with same project
70 | gpkg.write(qgs_path)
71 |
72 | curs.execute('SELECT name FROM qgis_projects')
73 | assert_equals('small_world.qgs', curs.fetchone()[0], 'small_world.qgs not found')
74 | assert curs.fetchone() is None
75 |
76 | curs.execute('SELECT name FROM qgis_resources')
77 | assert_equals('./qgis.png', curs.fetchone()[0], 'Image not found')
78 | assert curs.fetchone() is None
79 |
80 | curs.execute("SELECT scope FROM gpkg_extensions WHERE extension_name = 'qgis'")
81 | assert_equals('read-write', curs.fetchone()[0], 'Extension registration missing')
82 | assert curs.fetchone() is None
83 |
--------------------------------------------------------------------------------
/workflow-owc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pka/qgpkg/eb5e3c309ff97048799ce97b9d6499edde977a20/workflow-owc.png
--------------------------------------------------------------------------------
/workflow-qgis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pka/qgpkg/eb5e3c309ff97048799ce97b9d6499edde977a20/workflow-qgis.png
--------------------------------------------------------------------------------