This Processing Script is made available under the CC-0 license.
download_wcs_layer
from math import floor, ceil
from qgis.PyQt.QtCore import QCoreApplication, QVariant, QUrl, QUrlQuery
from qgis.PyQt.QtNetwork import QNetworkRequest, QNetworkReply
from qgis.core import (
QgsProcessing,
QgsProcessingAlgorithm,
QgsProcessingParameterString,
QgsProcessingParameterExtent,
QgsProcessingParameterNumber,
QgsProcessingParameterRasterLayer,
QgsProcessingParameterRasterDestination,
QgsProcessingContext,
QgsProcessingException,
QgsProcessingParameterExtent,
QgsCoordinateReferenceSystem,
QgsCoordinateTransform,
QgsRasterLayer,
QgsNetworkAccessManager,
QgsRectangle,
QgsProject,
QgsDataSourceUri,
)
from qgis.utils import iface
class DownloadWCSLayerAlgorithm(QgsProcessingAlgorithm):
"""
Downloads a WCS layer with a specified bounding box and resolution.
"""
# Algorithm metadata
def name(self):
return "download_wcs_layer"
def displayName(self):
return "Download WCS Layer"
def group(self):
return "Raster"
def groupId(self):
return "raster"
def shortHelpString(self):
return """
Downloads a WCS layer with a specified bounding box and resolution.
The bounding box is rounded to the resolution.
Uses QGIS Network Manager for HTTP requests.
"""
def initAlgorithm(self, config=None):
self.addParameter(
QgsProcessingParameterRasterLayer(
"wcs_layer",
"WCS Layer",
)
)
# Bounding box (extent)
self.addParameter(
QgsProcessingParameterExtent(
"extent",
"Bounding Box (Extent)",
defaultValue='',
)
)
# Output resolution (pixel size in CRS units)
self.addParameter(
QgsProcessingParameterNumber(
"resolution",
"Output Resolution (CRS units per pixel)",
type=QgsProcessingParameterNumber.Double,
defaultValue=1,
minValue=0.000001,
)
)
# Output raster layer
self.addParameter(
QgsProcessingParameterRasterDestination(
"output_raster",
"Output Raster Layer",
)
)
def processAlgorithm(self, parameters, context, feedback):
# WCS Layer
wcs_layer = self.parameterAsRasterLayer(parameters, "wcs_layer", context)
if not wcs_layer.providerType() == "wcs":
error_txt = f"Input layer is not a WCS layer: {wcs_layer.providerType()}"
raise QgsProcessingException(error_txt)
u = QgsDataSourceUri()
u.setEncodedUri(wcs_layer.source())
wcs_url = u.param('url')
wcs_coverage_id = u.param('identifier')
wcs_crs = u.param('crs')
# Resolution
resolution = self.parameterAsDouble(parameters, "resolution", context)
layer_minimum_resolution = min(wcs_layer.rasterUnitsPerPixelX(), wcs_layer.rasterUnitsPerPixelY())
if layer_minimum_resolution > resolution:
resolution = layer_minimum_resolution
feedback.pushInfo(f"Resolution raised to WCS minimum resolution: {resolution}")
# Extent
extent = self.parameterAsExtent(parameters, "extent", context)
extent_crs = self.parameterAsExtentCrs(parameters, "extent", context)
if not extent_crs == wcs_crs:
source_crs = QgsCoordinateReferenceSystem(extent_crs)
target_crs = QgsCoordinateReferenceSystem(wcs_crs)
transform = QgsCoordinateTransform(source_crs, target_crs, context.project())
extent = transform.transformBoundingBox(extent)
rounded_extent = self.round_extent_to_resolution(extent, resolution)
feedback.pushInfo(f"Rounded extent: {rounded_extent}")
# Output
output_path = self.parameterAsOutputLayer(parameters, "output_raster", context)
# Calculate width and height in pixels
width = int((rounded_extent.width() / resolution) + 0.5)
height = int((rounded_extent.height() / resolution) + 0.5)
feedback.pushInfo(f"Width: {width}, Height: {height}")
# Construct the WCS GetCoverage request
params = {
"service": "WCS",
"version": "1.0.0",
"request": "GetCoverage",
"coverage": wcs_coverage_id,
"format": "image/tiff",
"bbox": f"{rounded_extent.xMinimum()},{rounded_extent.yMinimum()},{rounded_extent.xMaximum()},{rounded_extent.yMaximum()}",
"crs": wcs_crs,
"response_crs": wcs_crs,
"width": width,
"height": height,
}
request_url = QUrl(wcs_url)
query = QUrlQuery()
for key, value in params.items():
query.addQueryItem(key, str(value))
request_url.setQuery(query)
network_manager = QgsNetworkAccessManager.instance()
request = QNetworkRequest(request_url)
# Send the request and handle the response
reply = network_manager.blockingGet(request)
if reply.error() != QNetworkReply.NetworkError.NoError:
error_txt = f"Network error: {reply.errorString()}"
raise QgsProcessingException(error_txt)
# Save the response to a TIFF file
with open(output_path, "wb") as f:
f.write(reply.content())
feedback.pushInfo(f"Downloaded TIFF file to: {output_path}")
# Load the raster layer into QGIS
layer = QgsRasterLayer(output_path, "WCS Layer")
if not layer.isValid():
error_txt = "Failed to load the downloaded raster layer."
raise QgsProcessingException(error_txt)
context.addLayerToLoadOnCompletion(
output_path,
QgsProcessingContext.LayerDetails(
"Downloaded WCS Layer",
context.project(),
"Downloaded WCS Layer",
),
)
return {"output_raster": output_path}
def round_extent_to_resolution(self, extent, resolution):
"""
Rounds the extent to a multiple of the resolution, but never shrinking the bbox.
"""
x_min = floor(extent.xMinimum() / resolution) * resolution
y_min = floor(extent.yMinimum() / resolution) * resolution
x_max = ceil(extent.xMaximum() / resolution) * resolution
y_max = ceil(extent.yMaximum() / resolution) * resolution
return QgsRectangle(x_min, y_min, x_max, y_max)
def createInstance(self):
"""
Required method to create a new instance of the algorithm.
"""
return DownloadWCSLayerAlgorithm()
# Register the algorithm
def classFactory(iface):
return DownloadWCSLayerAlgorithm()
Downloads a WCS layer with a specified bounding box and resolution. The bounding box is rounded to the resolution. Uses QGIS Network Manager for HTTP requests.
(This script is a work around for the native QGIS algorithm "Clip Raster by Extent" which does not work in combination with WCS layers. As reported here: https://github.com/qgis/QGIS/issues/67061)
This Processing Script is made available under the CC-0 license.
Hi Raymond, thank you so much for your contribution! I'm marking the script as Requiring Update: I tested it in QGIS 3.44.9 using the service you mentioned in the issue report, and it saves an output-raster.tif file with the server response in XML containing: <ServiceException code="MissingParameterValue" locator="crs">msWCSGetCoverage(): WCS server error. Required parameter CRS was not supplied.</ServiceException>. In QGIS 4.2.1, it returns the Python AttributeError: type object 'QNetworkReply' has no attribute 'NoError'.
Reviewed by Gabrieldeluca 6 days, 1 hour ago
Hi Raymond, thanks for the update. I can't get it to work. wcs_layer.source() doesn't seem to include a crs parameter. Therefore, wcs_crs stores an empty string, and the request_query for the new download includes empty crs and response_crs parameters, returning the same error as before.
Reviewed by Gabrieldeluca 3 days, 4 hours ago