@@ -236,12 +236,70 @@ def image_to_cog(source, dst_path=None, profile="deflate", **kwargs):
236236 cog_translate (source , dst_path , dst_profile , ** kwargs )
237237
238238
239+ def reproject (
240+ image , output , dst_crs = "EPSG:4326" , resampling = "nearest" , to_cog = True , ** kwargs
241+ ):
242+ """Reprojects an image.
243+
244+ Args:
245+ image (str): The input image filepath.
246+ output (str): The output image filepath.
247+ dst_crs (str, optional): The destination CRS. Defaults to "EPSG:4326".
248+ resampling (Resampling, optional): The resampling method. Defaults to "nearest".
249+ to_cog (bool, optional): Whether to convert the output image to a Cloud Optimized GeoTIFF. Defaults to True.
250+ **kwargs: Additional keyword arguments to pass to rasterio.open.
251+
252+ """
253+ import rasterio as rio
254+ from rasterio .warp import calculate_default_transform , reproject , Resampling
255+
256+ if isinstance (resampling , str ):
257+ resampling = getattr (Resampling , resampling )
258+
259+ image = os .path .abspath (image )
260+ output = os .path .abspath (output )
261+
262+ if not os .path .exists (os .path .dirname (output )):
263+ os .makedirs (os .path .dirname (output ))
264+
265+ with rio .open (image , ** kwargs ) as src :
266+ transform , width , height = calculate_default_transform (
267+ src .crs , dst_crs , src .width , src .height , * src .bounds
268+ )
269+ kwargs = src .meta .copy ()
270+ kwargs .update (
271+ {
272+ "crs" : dst_crs ,
273+ "transform" : transform ,
274+ "width" : width ,
275+ "height" : height ,
276+ }
277+ )
278+
279+ with rio .open (output , "w" , ** kwargs ) as dst :
280+ for i in range (1 , src .count + 1 ):
281+ reproject (
282+ source = rio .band (src , i ),
283+ destination = rio .band (dst , i ),
284+ src_transform = src .transform ,
285+ src_crs = src .crs ,
286+ dst_transform = transform ,
287+ dst_crs = dst_crs ,
288+ resampling = resampling ,
289+ ** kwargs ,
290+ )
291+
292+ if to_cog :
293+ image_to_cog (output , output )
294+
295+
239296def tms_to_geotiff (
240297 output ,
241298 bbox ,
242299 zoom = None ,
243300 resolution = None ,
244301 source = "OpenStreetMap" ,
302+ crs = "EPSG:3857" ,
245303 to_cog = False ,
246304 return_image = False ,
247305 overwrite = False ,
@@ -258,6 +316,7 @@ def tms_to_geotiff(
258316 resolution (float, optional): The resolution in meters. Defaults to None.
259317 source (str, optional): The tile source. It can be one of the following: "OPENSTREETMAP", "ROADMAP",
260318 "SATELLITE", "TERRAIN", "HYBRID", or an HTTP URL. Defaults to "OpenStreetMap".
319+ crs (str, optional): The output CRS. Defaults to "EPSG:3857".
261320 to_cog (bool, optional): Convert to Cloud Optimized GeoTIFF. Defaults to False.
262321 return_image (bool, optional): Return the image as PIL.Image. Defaults to False.
263322 overwrite (bool, optional): Overwrite the output file if it already exists. Defaults to False.
@@ -296,37 +355,22 @@ def tms_to_geotiff(
296355 return
297356
298357 xyz_tiles = {
299- "OPENSTREETMAP" : {
300- "url" : "https://tile.openstreetmap.org/{z}/{x}/{y}.png" ,
301- "attribution" : "OpenStreetMap" ,
302- "name" : "OpenStreetMap" ,
303- },
304- "ROADMAP" : {
305- "url" : "https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}" ,
306- "attribution" : "Google" ,
307- "name" : "Google Maps" ,
308- },
309- "SATELLITE" : {
310- "url" : "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}" ,
311- "attribution" : "Google" ,
312- "name" : "Google Satellite" ,
313- },
314- "TERRAIN" : {
315- "url" : "https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}" ,
316- "attribution" : "Google" ,
317- "name" : "Google Terrain" ,
318- },
319- "HYBRID" : {
320- "url" : "https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}" ,
321- "attribution" : "Google" ,
322- "name" : "Google Satellite" ,
323- },
358+ "OPENSTREETMAP" : "https://tile.openstreetmap.org/{z}/{x}/{y}.png" ,
359+ "ROADMAP" : "https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}" ,
360+ "SATELLITE" : "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}" ,
361+ "TERRAIN" : "https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}" ,
362+ "HYBRID" : "https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}" ,
324363 }
325364
326- if isinstance (source , str ) and source .upper () in xyz_tiles :
327- source = xyz_tiles [source .upper ()]["url" ]
328- elif isinstance (source , str ) and source .startswith ("http" ):
329- pass
365+ basemaps = get_basemaps ()
366+
367+ if isinstance (source , str ):
368+ if source .upper () in xyz_tiles :
369+ source = xyz_tiles [source .upper ()]
370+ elif source in basemaps :
371+ source = basemaps [source ]
372+ elif source .startswith ("http" ):
373+ pass
330374 else :
331375 raise ValueError (
332376 'source must be one of "OpenStreetMap", "ROADMAP", "SATELLITE", "TERRAIN", "HYBRID", or a URL'
@@ -528,7 +572,9 @@ def draw_tile(
528572 )
529573 if return_image :
530574 return image
531- if to_cog :
575+ if crs .upper () != "EPSG:3857" :
576+ reproject (output , output , crs , to_cog = to_cog )
577+ elif to_cog :
532578 image_to_cog (output , output )
533579 except Exception as e :
534580 raise Exception (e )
@@ -709,3 +755,148 @@ def draw_tile(source, lat0, lon0, lat1, lon1, zoom, filename, **kwargs):
709755 ** kwargs ,
710756 )
711757 return image
758+
759+
760+ def tiff_to_vector (tiff_path , output , simplify_tolerance = None , ** kwargs ):
761+ """Convert a tiff file to a gpkg file.
762+
763+ Args:
764+ tiff_path (str): The path to the tiff file.
765+ output (str): The path to the vector file.
766+ simplify_tolerance (float, optional): The maximum allowed geometry displacement.
767+ The higher this value, the smaller the number of vertices in the resulting geometry.
768+ """
769+
770+ with rasterio .open (tiff_path ) as src :
771+ band = src .read ()
772+
773+ mask = band != 0
774+ shapes = features .shapes (band , mask = mask , transform = src .transform )
775+
776+ fc = [
777+ {"geometry" : shapely .geometry .shape (shape ), "properties" : {"value" : value }}
778+ for shape , value in shapes
779+ ]
780+ if simplify_tolerance is not None :
781+ for i in fc :
782+ i ["geometry" ] = i ["geometry" ].simplify (tolerance = simplify_tolerance )
783+
784+ gdf = gpd .GeoDataFrame .from_features (fc )
785+ if src .crs is not None :
786+ gdf .set_crs (crs = src .crs , inplace = True )
787+ gdf .to_file (output , ** kwargs )
788+
789+
790+ def tiff_to_gpkg (tiff_path , output , simplify_tolerance = None , ** kwargs ):
791+ """Convert a tiff file to a gpkg file.
792+
793+ Args:
794+ tiff_path (str): The path to the tiff file.
795+ output (str): The path to the gpkg file.
796+ simplify_tolerance (float, optional): The maximum allowed geometry displacement.
797+ The higher this value, the smaller the number of vertices in the resulting geometry.
798+ """
799+
800+ if not output .endswith (".gpkg" ):
801+ output += ".gpkg"
802+
803+ tiff_to_vector (tiff_path , output , simplify_tolerance = simplify_tolerance , ** kwargs )
804+
805+
806+ def tiff_to_shp (tiff_path , output , simplify_tolerance = None , ** kwargs ):
807+ """Convert a tiff file to a shapefile.
808+
809+ Args:
810+ tiff_path (str): The path to the tiff file.
811+ output (str): The path to the shapefile.
812+ simplify_tolerance (float, optional): The maximum allowed geometry displacement.
813+ The higher this value, the smaller the number of vertices in the resulting geometry.
814+ """
815+
816+ if not output .endswith (".shp" ):
817+ output += ".shp"
818+
819+ tiff_to_vector (tiff_path , output , simplify_tolerance = simplify_tolerance , ** kwargs )
820+
821+
822+ def tiff_to_geojson (tiff_path , output , simplify_tolerance = None , ** kwargs ):
823+ """Convert a tiff file to a GeoJSON file.
824+
825+ Args:
826+ tiff_path (str): The path to the tiff file.
827+ output (str): The path to the GeoJSON file.
828+ simplify_tolerance (float, optional): The maximum allowed geometry displacement.
829+ The higher this value, the smaller the number of vertices in the resulting geometry.
830+ """
831+
832+ if not output .endswith (".geojson" ):
833+ output += ".geojson"
834+
835+ tiff_to_vector (tiff_path , output , simplify_tolerance = simplify_tolerance , ** kwargs )
836+
837+
838+ def get_xyz_dict (free_only = True ):
839+ """Returns a dictionary of xyz services.
840+
841+ Args:
842+ free_only (bool, optional): Whether to return only free xyz tile services that do not require an access token. Defaults to True.
843+
844+ Returns:
845+ dict: A dictionary of xyz services.
846+ """
847+ import collections
848+ import xyzservices .providers as xyz
849+
850+ def _unpack_sub_parameters (var , param ):
851+ temp = var
852+ for sub_param in param .split ("." ):
853+ temp = getattr (temp , sub_param )
854+ return temp
855+
856+ xyz_dict = {}
857+ for item in xyz .values ():
858+ try :
859+ name = item ["name" ]
860+ tile = _unpack_sub_parameters (xyz , name )
861+ if _unpack_sub_parameters (xyz , name ).requires_token ():
862+ if free_only :
863+ pass
864+ else :
865+ xyz_dict [name ] = tile
866+ else :
867+ xyz_dict [name ] = tile
868+
869+ except Exception :
870+ for sub_item in item :
871+ name = item [sub_item ]["name" ]
872+ tile = _unpack_sub_parameters (xyz , name )
873+ if _unpack_sub_parameters (xyz , name ).requires_token ():
874+ if free_only :
875+ pass
876+ else :
877+ xyz_dict [name ] = tile
878+ else :
879+ xyz_dict [name ] = tile
880+
881+ xyz_dict = collections .OrderedDict (sorted (xyz_dict .items ()))
882+ return xyz_dict
883+
884+
885+ def get_basemaps (free_only = True ):
886+ """Returns a dictionary of xyz basemaps.
887+
888+ Args:
889+ free_only (bool, optional): Whether to return only free xyz tile services that do not require an access token. Defaults to True.
890+
891+ Returns:
892+ dict: A dictionary of xyz basemaps.
893+ """
894+
895+ basemaps = {}
896+ xyz_dict = get_xyz_dict (free_only = free_only )
897+ for item in xyz_dict :
898+ name = xyz_dict [item ].name
899+ url = xyz_dict [item ].build_url ()
900+ basemaps [name ] = url
901+
902+ return basemaps
0 commit comments