""" Module to implement TMS services as a protocol. """ import os import re import tempfile import shutil # Fix gdal 2.40 and 3.3 integration problems try: import ogr import osr except ModuleNotFoundError: from osgeo import ogr, osr from tools import const from tools.protocols.masterProtocol import MasterProtocol M2RAD = 180.0 / (3.1416 * 6378388) MAX_DIST = 1000 class TMS(MasterProtocol): """ Class to control de TMS protocol Attributes ---------- CONNECTION_REQUIRED: dict Mapping of required connection parameters. Only is important when GMS is used with Itasker. NAME: str Protocol identifier string. Parameters ---------- ip: str Url of service """ CONNECTION_REQUIRED = {'fileType': 'driver_type', 'ip': 'ip', 'source': 'source', 'port': 'port', 'domain': 'domain', 'user': 'user', 'password': 'password', 'subtype': 'type', 'layerName': 'layer_name'} NAME = const.TMS_KEY def __init__(self, ip, port, user, password, *_, **__): super().__init__() ip = re.sub(r'(?:(?}', ip) self.ip = ip.replace("$", r"\$") self.port = port self.user = user self.password = password self.cred_string = "{user}:{password}"\ .format(user=user, password=password)*bool(user) self.gdal_wms_conf = "" \ "" \ " {url}" \ "" \ "" \ " -20037508.34" \ " 20037508.34" \ " 20037508.34" \ " -20037508.34" \ " 21" \ " 1" \ " 1" \ " top" \ "" \ "EPSG:3857" \ "256" \ "256" \ "3" + \ self.cred_string + \ "204,404,500" \ "true" \ "" def exists(self, _): """ Check if the layer exists. Always return True. Returns ------- """ return True def upload(self, *_, **__): """ Upload data to a TMS service is not allowed """ return None def gdal_layer(self, *_): """ Get gdal connection string to open the service. """ return self.gdal_wms_conf.format(url=self.ip, credentials=self.cred_string) def gdal_url(self, *_, **__): """ Get gdal connection string to open the service. In that case is exactly the same as gdal_layer """ return self.gdal_wms_conf.format(url=self.ip, credentials=self.cred_string) def is_file(self, path): """ Here is not used """ return True def download(self, src_path, dst, *_, **kwargs): """ Download method. Due to a TMS service could be very big, a crop by a vector layer must be done and download the resulting crop. Parameters ---------- src_path: str Source pointing to the layer, in this method it is ignored. dst: str Local path to download the file. kwargs: dict Dictionary containing: - vector: Layer object containing a vector layer to crop the service - res: float number pointing the pixel per meter of the resulting crop. Returns ------- str: Path to downloaded raster. """ tmp_dir = tempfile.mkdtemp(dir=os.path.dirname(dst)) ds_vector = ogr.Open(kwargs['vector'].gdal_layer()) ds_layer = list(ds_vector)[0] osr_3857 = osr.SpatialReference() osr_3857.ImportFromEPSG(3857) is_3857 = ds_layer.GetSpatialRef().ExportToWkt() == osr_3857.ExportToWkt() extent = ds_layer.GetExtent() del ds_vector if not is_3857: # Change srs of vector layer from original to 4326, working with WKT srs reprojected_source = "{}/reprojected.geojson".format(tmp_dir) command = "ogr2ogr -overwrite -t_srs \"{t_srs}\" \"{dst}\" \"{src}\"" os.system(command.format(dst=reprojected_source, src=kwargs['vector'].gdal_layer(), t_srs=osr_3857.ExportToWkt().replace('"', '\\"')), ) kwargs['vector'].update_driver_and_protocol({ 'protocol': 'LOCAL', 'type': const.VECTOR_KEY, 'ip': '0.0.0.0', 'user': '', 'password': '', 'port': 99999, 'source': reprojected_source, 'driver_type': const.GJSON_KEY, 'layer_name': reprojected_source.split(os.sep)[-1].split('.')[0] }) ds = ogr.Open(kwargs['vector'].gdal_layer()) extent = list(ds)[0].GetExtent() del ds # # First creating a vrt with the bbox of the crop os.system("gdal_translate \"{raster_layer}\" {tmp_dir}/cropped.vrt " "-projwin {ulx} {uly} {lrx} {lry} -tr {resolution} {resolution}" .format(raster_layer=self.gdal_url(src_path), ulx=extent[0], uly=extent[3], lrx=extent[1], lry=extent[2], resolution=kwargs['res'], tmp_dir=tmp_dir)) # # Second: calculate size of tiles to be created. The reason to do that is # request small tiles to the WMS service, avoiding some limitation. # The requested tiles will not be bigger than MAX_DIST ruled_by = min(extent[1] - extent[0], extent[3] - extent[2]) os.mkdir('{tmp_dir}/tiles'.format(tmp_dir=tmp_dir)) os.system("gdal_retile.py -s_srs \"{s_srs}\" -ps {pixel_size} {pixel_size} " "-targetDir {tmp_dir}/tiles {tmp_dir}/cropped.vrt" .format(s_srs=osr_3857.ExportToWkt().replace('"', '\\"'), pixel_size=int((ruled_by / (ruled_by//MAX_DIST + 1)) / kwargs['res']), tmp_dir=tmp_dir)) # Make a vrt of tiles to crop all with the desired vector layer os.system("gdalbuildvrt -tr {resolution} {resolution} " "{tmp_dir}/merge.vrt {tmp_dir}/tiles/*".format(resolution=kwargs['res'], tmp_dir=tmp_dir)) # Apply crop os.system("gdalwarp -q -overwrite -dstalpha -cutline \"{layer_vector}\" " "-crop_to_cutline -tr {resolution} {resolution} {tmp_dir}/merge.vrt {dst} " "-co BIGTIFF=YES -co TILED=YES " "--config CHECK_DISK_FREE_SPACE FALSE" .format(layer_vector=kwargs['vector'].gdal_layer(), tmp_dir=tmp_dir, dst=os.path.join(tmp_dir, 'before_reproject.vrt'), resolution=kwargs['res'])) # Change to EPS:4326 os.system("gdalwarp -q -overwrite -t_srs 'EPSG:4326' " "-dstalpha {tmp_dir}/before_reproject.vrt {dst} " "-co BIGTIFF=YES -co TILED=YES " "--config CHECK_DISK_FREE_SPACE FALSE".format(tmp_dir=tmp_dir, dst=dst)) shutil.rmtree("{}".format(tmp_dir)) return dst def format_parameters(self, _, parameters): """ Method to format the parameters to work with TMS protocol. Nothing is required, Parameters ---------- _ parameters: dict Parameters of the protocol Returns ------- dict: Parameters """ return parameters