/
giscloud_uploader_algorithm.py
414 lines (367 loc) · 16.3 KB
/
giscloud_uploader_algorithm.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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
361
362
363
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
# -*- coding: utf-8 -*-
"""
***************************************************************************
GISCloudUpload
A QGIS plugin
Uploader to GIs cloud
-------------------
begin : 2015-11-23
copyright : (C) 2015 by Spatial Vision
email : michael.king@spatialvision.com.au
***************************************************************************/
/***************************************************************************
* *
* 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 glob import glob
from itertools import chain
from functools import partial
import re
from qgis.utils import iface
from qgis.core import (
QgsCoordinateTransform, QgsCoordinateReferenceSystem,
QgsCsException, QgsRectangle, QgsRasterLayer, QGis)
from processing.core.GeoAlgorithm import GeoAlgorithm
from processing.core.parameters import (
ParameterMultipleInput, ParameterString,
ParameterBoolean, ParameterExtent)
from processing.core.ProcessingConfig import ProcessingConfig
from processing.core.ProcessingLog import ProcessingLog
from processing.tools import system, dataobjects
from giscloud_utils import GISCloudUtils
import os.path
from modules import requests #sadly, we can't guarantee that requests is installed in your qgis machine.
import zipfile
import json
__author__ = 'Spatial Vision'
__date__ = '2015-11-23'
__copyright__ = '(C) 2015 by Spatial Vision'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
class GISCloudUploadAlgorithm(GeoAlgorithm):
"""
The functional algorithm for uploading QGIS environment workspace layers
to a GIS Cloud account.
"""
# Constants used to refer to parameters and outputs. They will be
# used when calling the algorithm from another algorithm, or when
# calling from the QGIS console.
INPUT_LAYER_VECTOR = 'INPUT_LAYER_VECTOR'
INPUT_LAYER_RASTER = "INPUT_LAYER_RASTER"
OUTPUT_FOLDER = 'OUTPUT_FOLDER'
MAP_NAME = "MAP_NAME"
CHOOSE_MAP = "CHOOSE_MAP"
MAP_EXTENT = "MAP_EXTENT"
def getIcon(self):
"""
Get the icon.
"""
return GISCloudUtils.getIcon()
def defineCharacteristics(self):
"""
Here we define the inputs and output of the algorithm, along
with some other properties.
"""
# The name that the user will see in the toolbox
self.name = 'GIS Cloud Uploader'
# The branch of the toolbox under which the algorithm will appear
self.group = 'Web'
# We add the input vector layer. It can have any kind of geometry
# It is a mandatory (not optional) one, hence the False argument
self.addParameter(ParameterMultipleInput(
self.INPUT_LAYER_VECTOR, # tool parameter to store the input under.
self.tr('Vector layers to upload to GISCloud'), # name as it appears in the window
ParameterMultipleInput.TYPE_VECTOR_ANY, # Either raster or vector
True # Not optional
))
self.addParameter(ParameterMultipleInput(
self.INPUT_LAYER_RASTER, # tool parameter to store the input under.
self.tr('Raster layers to upload to GISCloud'), # name as it appears in the window
ParameterMultipleInput.TYPE_RASTER, # Either raster or vector
True # Not optional
))
self.addParameter(ParameterString(
self.OUTPUT_FOLDER, # specify the name of the folder in which to
# store the uploaded layers
"GISCloud output folder name",
default="QGIS upload" # default value for the folder to be uploaded
))
self.addParameter(ParameterBoolean(
self.CHOOSE_MAP, # determine if you want to upload the files to a new map
"Would you like to add the uploaded files to a new map? \n"
"*Leave empty if you do not want to produce a new map with "
"your uploaded layers*",
default=False # default not to upload to a new map
))
self.addParameter(ParameterString(
self.MAP_NAME, # specify the map name to upload the files into
"GIS Cloud output map name",
default="Map Project" # default map name
))
self.addParameter(ParameterExtent( # specify the extent you wish the new map to have
self.MAP_EXTENT, "Upload map extent"
))
def processAlgorithm(self, progress):
"""
Here is where the processing itself takes place.
:param progress: Interface to the processing window
:type progress: processing.gui.AlgorithmDialog.AlgorithmDialog
"""
# The first thing to do is retrieve the values of the parameters
# entered by the user
# the api key is derived from the optional settings value entered
api_key = ProcessingConfig.getSetting(GISCloudUtils.GISCloud_character)
if not re.match("^[a-f0-9]{32}$", api_key):
progress.setInfo("Please set the API Key for the GISCloud plugin", error=True)
raise ValueError("No API Key set for the GISCloud uploader")
input_filenames = filter(
partial(self.check_extension, progress=progress), # Aliases a function to pre-define an argument
chain( # producing a list from the selected layers to upload
(self.getParameterValue(self.INPUT_LAYER_VECTOR) or "").split(";"),
(self.getParameterValue(self.INPUT_LAYER_RASTER) or "").split(";")
)
)
progress.setInfo("Gathering layers")
progress.setPercentage(0)
output_filename = self.getParameterValue(self.OUTPUT_FOLDER)
map_created = str(self.getParameterValue(self.CHOOSE_MAP))
map_name = self.getParameterValue(self.MAP_NAME)
rest_endpoint = "https://api.giscloud.com/1/"
base_headers = {
"API-Version": 1,
"API-Key": api_key,
}
# details the rest endpoint urls for the map creation,
# layer upload to map and the file for the layers to be uploaded to
map_url = rest_endpoint + "maps.json"
layer_url = rest_endpoint + "layers.json"
storage_url = rest_endpoint + "storage/fs/" + output_filename
# This first check establishes if the user
# has entered anything for uploading and returns nothing
if not input_filenames:
ProcessingLog.addToLog(
ProcessingLog.LOG_WARNING,
"No valid datasets found to upload"
)
return
# Assesses if the user has chooses to
# create a new map with the upload
if map_created == 'True':
bounds = self.bounds() # seeks to determine the extent of the map
progress.setInfo("Creating empty map")
mid = self.create_map(map_url, base_headers, map_name, bounds) # Map ID
else:
# provides feedback on the options taken
# by the user and where the data will appear
ProcessingLog.addToLog(
ProcessingLog.LOG_INFO,
"No map will be created, all files will be added "
"to the file manager in" + output_filename)
mid = None
# sends each of the selected files to be individually processed
# and entered into the giscloud file manager folder
progress.setInfo("Uploading files")
for i, path in enumerate(input_filenames, 1):
percent = float(i) / (len(input_filenames) + 1) * 100
progress.setPercentage(int(percent))
progress.setInfo("Uploading " + path)
self.upload_to_filemanager(storage_url, base_headers, path)
if mid:
self.add_layer_to_map(mid, path, output_filename, layer_url, base_headers)
# validation and information feedback on the success of the upload process
progress.setPercentage(100)
ProcessingLog.addToLog(
ProcessingLog.LOG_INFO,
"Uploaded all valid datasets to the GIS Cloud folder " + output_filename
)
def help(self):
"""
Get the help documentation for this algorithm.
:return: Help text is html from string, the help html
:rtype: bool, str
"""
# this is contained in the root folder
# GISCloudUpload/doc/Publishing instructions.html
# help can be found on the right tab in the toolbox
help_data = open(os.path.join(
os.path.dirname(__file__),
"doc",
"Publishing_instructions.html"
)).read()
return True, help_data
def upload_to_filemanager(self, storage_url, base_headers, path):
"""
# Sequentially uploads each of the selected files to
# the specified file name in GIS Clouds file manager
:param storage_url:
:param base_headers:
:param path:
:return:
"""
# Extracts the basename of the file, collects all
# associated file type extensions and zips the files for upload
zip_path = system.getTempFilename("zip")
with zipfile.ZipFile(zip_path, "w") as ziptofm:
for pathname in glob(os.path.splitext(path)[0] + ".*"):
ziptofm.write(pathname, os.path.basename(pathname))
# read binary and post file with GIS Cloud REST API
ziptofm = {'file': open(zip_path, 'rb')}
post = requests.post(storage_url, headers=base_headers, files=ziptofm, verify=False)
post.raise_for_status()
# feedback on the uploaded datasets as well as the file extensions
ProcessingLog.addToLog(
ProcessingLog.LOG_INFO,
"Uploaded {}".format(path)
)
def check_extension(self, path, progress):
"""
Due to the wider range of the accepted file types
by QGIS the file extensions are checked and
given a boolean value for further processing if they are accepted by GIS Cloud
:param path: Path to check if valid dataset
:type path: str
:param progress: Interface to the processing window
:type progress: processing.gui.AlgorithmDialog.AlgorithmDialog
:rtype: bool
"""
filename, file_extension = os.path.splitext(path)
if file_extension in (".shp", ".mif", ".mid", ".tab", ".kml", ".gpx",
".tif", ".tiff", ".ecw", ".img", ".jp2",
".jpg", ".png", ".pdf", ".json", ".geojson"):
return True
else:
# progress.error(path + " is not compatible with GISCloud.")
# provides feedback on the rejection of a filetype
if path: # i.e. not an empty string
progress.error(path + " is not compatible with GISCloud.")
ProcessingLog.addToLog(
ProcessingLog.LOG_WARNING,
"{} is not an accepted filetype".format(path)
)
return False
def create_map(self, map_url, base_headers, map_name, bounds):
"""
Creates a new map in GIS Cloud if the
user has specified the need for one.
:param map_url:
:param base_headers:
:param map_name:
:param bounds: A QgsRectange object
:return:
"""
# Appends the contentType command to the headers
headers = {
"ContentType": "application/json"
}
headers.update(base_headers)
# gathers the SRC bounds of the associated uploaded layers and
# converts them into WGS 84 (EPSG:4326)
map_data = {
"name": map_name,
"bounds": json.dumps({
"xmin": bounds.xMinimum(),
"xmax": bounds.xMaximum(),
"ymin": bounds.yMinimum(),
"ymax": bounds.yMaximum(),
}),
"description": "Description",
"proj4": "+init=epsg:4326",
"epsg": 4326,
"copyright": "Spatial Vision"
}
# GIS Cloud REST command for generating a new map
map_post = requests.post(map_url, headers=base_headers,
data=json.dumps(map_data), verify=False)
# Extracting the newly generated Map ID from the JSON
# output for use in the upload_to_map layers REST endpoint
mid = int(map_post.headers['Location'].split("/")[-1])
ProcessingLog.addToLog(
ProcessingLog.LOG_INFO,
map_name + " was successfully uploaded to your account")
return mid
def bounds(self):
"""
Function for recalculating the bounded extents of the
layers as they are processed. Under construction
:return:
"""
# Requires refinement for raster manipulation of bounds
selected_extent = unicode(self.getParameterValue(self.MAP_EXTENT)).split(',')
xMin = float(selected_extent[0])
xMax = float(selected_extent[1])
yMin = float(selected_extent[2])
yMax = float(selected_extent[3])
extent = QgsRectangle(xMin, yMin, xMax, yMax)
mapCRS = iface.mapCanvas().mapSettings().destinationCrs()
transform = QgsCoordinateTransform(
mapCRS,
QgsCoordinateReferenceSystem('EPSG:4326') # WGS84
# QgsCoordinateReferenceSystem('EPSG:3785') # Popular vis mercator
)
try:
layer_extent = transform.transform(extent)
except QgsCsException:
ProcessingLog.addToLog(ProcessingLog.LOG_WARNING,
"exception in transform layer srs")
layer_extent = QgsRectangle(-180, -90, 180, 90)
ProcessingLog.addToLog(ProcessingLog.LOG_INFO, layer_extent.toString())
return layer_extent
def get_layer_geomtype(self, uri):
"""
Passed in the path that's generated by Processing, determine whether the layer is
point, line, polygon or raster and return that.
:param uri: The URI generated by the processing input
:type uri: str
:return: One of point, line, polygon, or raster
:rtype: str
"""
layer = dataobjects.getObjectFromUri(uri, False)
if isinstance(layer, QgsRasterLayer):
return "raster"
geom_type = layer.geometryType()
geom_const = {
QGis.Point: "point",
QGis.Line: "line",
QGis.Polygon: "polygon"
}
try:
return geom_const[geom_type]
except KeyError:
raise IOError("Geometry type of %s not supported" % uri)
def add_layer_to_map(self, mid, path, output_filename, layer_url, base_headers):
"""
Providing the uploaded layers sequentially to the recently made map
Naming the uploaded layer in the map by the basename of the file
:param mid:
:param path:
:param output_filename:
:param layer_url:
:param base_headers:
:return:
"""
# Appending the necessary contentType to the REST API post
basename = os.path.basename(path)
geomtype = self.get_layer_geomtype(path)
headers = {
"ContentType": "application/json"
}
headers.update(base_headers)
# Providing the base JSON format to upload the layers to the map
layer_data = {
"mid": mid,
"name": basename,
"type": geomtype,
"source": json.dumps({
"type": "file",
"src": "/" + output_filename + "/" + basename,
"name": basename
})}
# The rest API call to upload the layers to the map
layers_post = requests.post(layer_url, headers=base_headers,
data=json.dumps(layer_data), verify=False)
layers_post.raise_for_status()