I'm working on a tool that performs some computations using elevations from a DTM file retrieved from a WCS service. These computations will be done on predefined regions of Europe.
I'm trying to encapsulate some things in classes, but it is my first time really using them, so I would love to get some feedback.
Basically I created a Region
class that contains several attributes and internal methods that connects to the WCS service and handles the response.
Since the regions are predefined, I also created 2 additional classes that store constants: BboxFlood
, that contains the BBOX
I want to retrieve from the service (and will be used to do some flooding operations) and Borders
, which contains the outline of the region (which is the real region of interest). This outline is used to create a mask (masked_raster
) which I will modify using other functions lately.
import matplotlib.pyplot as plt
import numpy as np
from requests.exceptions import MissingSchema
from owslib.wcs import WebCoverageService
from owslib.util import ServiceException
from rasterio.io import MemoryFile
from rasterio.errors import RasterioIOError
from rasterio.plot import show
from geopandas import read_file
from rasterio.features import geometry_mask
from shapely.geometry import mapping
from fiona.errors import DriverError
class Region():
def __init__(self, name, bbox, epsg, outline):
"""
Create a region of interest
Parameters
----------
name : str
Region name.
bbox : tuple or list
Bounding box of the region
epsg : str
Spatial reference system
outline : shapely.geometry.polygon.Polygon or str, optional
A shapely Polygon defining the border of the region of interest.
Alternatively, a path to the shapefile.
NOTE: The outline epsg should coincide with the raster one.
Returns
-------
None.
"""
self.name = name
self.bbox = bbox
self.epsg = epsg
self.outline = outline
self.raster = None
self.masked_raster = None
self.mask = None
self.profile = None
self.transform = None
self.resx = None
self.resy = None
if isinstance(outline, str):
self._read_outline(outline)
def __str__(self):
return f"Region: {self.name}, Bbox: {self.bbox}, EPSG: {self.epsg}"
def __repr__(self):
return f"Region({self.name!r}, {self.bbox!r}, {self.epsg!r})"
def _get_bytes_from_response(self, wcs_response):
"""
Get tif file from multipart response from DV
Parameters
----------
wcs_response : owslib.util.ResponseWrapper
wcs response after getCoverage
Returns
-------
image_bytes : bytes
image file in bytes
"""
boundary = b"--wcs"
bytes_parts = wcs_response.read().split(boundary)
for part in bytes_parts:
if part.startswith(b"\nContent-Type: image/tiff"):
index = part.index(b"II*")
image_bytes = part[index:]
break
return image_bytes
def _bytes_to_raster(self, response_as_bytes):
# This internal method is used to read the response retrieved from
# the WCS service
try:
# Create a file in memory not to write any file to the hard disk
with MemoryFile(response_as_bytes) as memfile:
with memfile.open() as src:
raster = src.read(1)
profile = src.profile
res = src.res
except RasterioIOError() as e:
print("Rasterio error:", e)
except Exception as e:
print('Unexpected error:', e)
else:
self.raster = raster
self.profile = profile
self.transform = profile['transform']
self.resx = res[0]
self.resy = res[1]
def _read_outline(self, outline_path):
# Read outline from path if not given as a Polygon
try:
gdf = read_file(outline_path)
except DriverError as e:
print("Incorrect file name:", e)
else:
self.outline = gdf.geometry.iloc[0]
def _set_masked_raster(self):
# Create a masked version of the raster based on outline
mask = geometry_mask([mapping(self.outline)],
out_shape=self.raster.shape,
transform=self.transform,
invert=True
)
self.mask = ~mask
self.masked_raster = np.where(mask,
self.raster,
self.profile['nodata'])
def get_dtm_from_wcs_201(self,
raster_source='https://geo.api.vlaanderen.be/DHMV/wcs',
layer='DHMVII_DTM_1m',
format='image/tiff',
version="2.0.1",
scalefactor=3):
"""
Get raster and metadata from a WCS service
Parameters
----------
raster_source : str, optional
URL of WCS service.
layer : str, optional
CoverageID.
format : str, optional
File type of the request. The default is 'image/tiff'.
version : str, optional
Version of the WCS protocol. The default is "2.0.1".
scalefactor : int, optional
Scale. The default is 3.
Returns
-------
None.
"""
sset = [('x', self.bbox[0], self.bbox[2]), ('y', self.bbox[1], self.bbox[3])]
try:
# Connect to WCS and try getting a coverage
wcs = WebCoverageService(raster_source, version=version)
response = wcs.getCoverage(identifier=[layer],
bbox=self.bbox,
crs=self.epsg,
subsets=sset,
format=format,
scalefactor=scalefactor,
)
except MissingSchema as e:
print('Invalid URL:', e)
except ServiceException as e:
print("Service error:", e)
except Exception as e:
print("An unexpected error has ocurred", e)
else:
image_bytes = self._get_bytes_from_response(response)
self._bytes_to_raster(image_bytes)
self._set_masked_raster()
def reset_masked_raster(self):
self._set_masked_raster()
from shapely.geometry import Polygon
class BboxFlood():
ROMBOUT = {'bbox': (71274, 213729, 75273, 219336),
'epsg': 'EPSG:31370'}
class Borders():
ROMBOUT = {'outline': Polygon([(71275.24244585728, 214206.7075793226),
(71332.89192388157, 214301.1237481891),
(71451.59618105175, 214522.12153573174),
(71542.7001391952, 214700.03943770562),
(71605.07543163445, 214832.82320418613),
(71750.3815106122, 215099.33581733558),
(71788.18471815114, 215162.6561899633),
(71811.33918276873, 215193.37129608868),
(71853.86779125003, 215234.00974419303),
(71886.47305775236, 215265.19739041265),
(71909.15498227572, 215310.56123945938),
(71914.82546340655, 215363.9582701081),
(71920.02340444316, 215416.88276066261),
(71928.05658604519, 215456.57612857848),
(71937.50738792992, 215485.40107432692),
(71965.38725348988, 215542.1058856353),
(72009.33348225389, 215614.87706014776),
(72072.18131478736, 215710.8026992778),
(72097.69847987614, 215755.69400823026),
(72152.04059071335, 215863.43314971623),
(72178.03029589636, 215913.99493979954),
(72187.95363787533, 215954.6333879039),
(72195.04173928888, 215999.05215676213),
(72187.4810977811, 216064.2626897668),
(72169.99711429434, 216140.81418503314),
(72168.57949401163, 216214.0578996398),
(72180.86553646179, 216347.31420621453),
(72189.84379825227, 216402.128857146),
(72197.8769798543, 216429.53618261172),
(72221.97652466036, 216482.93321326046),
(72264.26886309455, 216559.01216843256),
(72300.18191025654, 216626.58540190838),
(72334.20479704157, 216687.54307406492),
(72368.22768382661, 216733.8520033001),
(72415.95423334451, 216787.72157404307),
(72501.95653049558, 216866.16322968635),
(72556.77118142703, 216924.28566127748),
(72574.25516491377, 216948.8577461778),
(73971.55622357152, 219165.54332824182),
(74158.68210088921, 219335.65776216701),
(74694.54256775357, 218468.07414914857),
(74856.1512799825, 218306.46543691962),
(75272.9316430992, 217991.75373415803),
(72559.60642199244, 214402.33917833655),
(71726.04569575902, 213730.38716433206),
(71275.24244585728, 214206.7075793226)]),
'epsg':'EPSG:31370'}
# Example
rombout = Region("Romb", **BboxFlood.ROMBOUT,
outline=Borders.ROMBOUT['outline'])
rombout.get_dtm_from_wcs_201(scalefactor=5)
I have another module that contains functions that takes a region as parameter (they modify the masked_raster
attribute)
What do you think? Is it well-structured? One of my biggest concern is how to test it (I have been doing it manually).
Any feedback is welcomed.