common module¶
This module contains some common functions for both folium and ipyleaflet.
The_national_map_USGS
¶
The national map is a collection of topological datasets, maintained by the USGS.
It provides an API endpoint which can be used to find downloadable links for the products offered. - Full description of datasets available can retrieved. This consists of metadata such as detail description and publication dates. - A wide range of dataformats are available
This class is a tiny wrapper to find and download files using the API.
More complete documentation for the API can be found at https://apps.nationalmap.gov/tnmaccess/#/
Source code in leafmap/common.py
class The_national_map_USGS:
"""
The national map is a collection of topological datasets, maintained by the USGS.
It provides an API endpoint which can be used to find downloadable links for the products offered.
- Full description of datasets available can retrieved.
This consists of metadata such as detail description and publication dates.
- A wide range of dataformats are available
This class is a tiny wrapper to find and download files using the API.
More complete documentation for the API can be found at
https://apps.nationalmap.gov/tnmaccess/#/
"""
def __init__(self):
self.api_endpoint = r"https://tnmaccess.nationalmap.gov/api/v1/"
self.DS = self.datasets_full
@property
def datasets_full(self) -> list:
"""
Full description of datasets provided.
Returns a JSON or empty list.
"""
link = f"{self.api_endpoint}datasets?"
try:
return requests.get(link).json()
except Exception:
print(f"Failed to load metadata from The National Map API endpoint\n{link}")
return []
@property
def prodFormats(self) -> list:
"""
Return all datatypes available in any of the collections.
Note that "All" is only peculiar to one dataset.
"""
return set(i["displayName"] for ds in self.DS for i in ds["formats"])
@property
def datasets(self) -> list:
"""
Returns a list of dataset tags (most common human readable self description for specific datasets).
"""
return set(y["sbDatasetTag"] for x in self.DS for y in x["tags"])
def parse_region(self, region, geopandas_args={}) -> list:
"""
Translate a Vector dataset to its bounding box.
Args:
region (str | list): an URL|filepath to a vector dataset to a polygon
geopandas_reader_args (dict, optional): A dictionary of arguments to pass to the geopandas.read_file() function.
Used for reading a region URL|filepath.
"""
import geopandas as gpd
if isinstance(region, str):
if region.startswith("http"):
region = github_raw_url(region)
region = download_file(region)
elif not os.path.exists(region):
raise ValueError("region must be a path or a URL to a vector dataset.")
roi = gpd.read_file(region, **geopandas_args)
roi = roi.to_crs(epsg=4326)
return roi.total_bounds
return region
def download_tiles(
self, region=None, out_dir=None, download_args={}, geopandas_args={}, API={}
) -> None:
"""
Download the US National Elevation Datasets (NED) for a region.
Args:
region (str | list, optional): An URL|filepath to a vector dataset Or a list of bounds in the form of [minx, miny, maxx, maxy].
Alternatively you could use API parameters such as polygon or bbox.
out_dir (str, optional): The directory to download the files to. Defaults to None, which uses the current working directory.
download_args (dict, optional): A dictionary of arguments to pass to the download_file function. Defaults to {}.
geopandas_args (dict, optional): A dictionary of arguments to pass to the geopandas.read_file() function.
Used for reading a region URL|filepath.
API (dict, optional): A dictionary of arguments to pass to the self.find_details() function.
Exposes most of the documented API. Defaults to {}.
Returns:
None
"""
if os.environ.get("USE_MKDOCS") is not None:
return
if out_dir is None:
out_dir = os.getcwd()
else:
out_dir = os.path.abspath(out_dir)
tiles = self.find_tiles(
region, return_type="list", geopandas_args=geopandas_args, API=API
)
T = len(tiles)
errors = 0
done = 0
for i, link in enumerate(tiles):
file_name = os.path.basename(link)
out_name = os.path.join(out_dir, file_name)
if i < 5 or (i < 50 and not (i % 5)) or not (i % 20):
print(f"Downloading {i+1} of {T}: {file_name}")
try:
download_file(link, out_name, **download_args)
done += 1
except KeyboardInterrupt:
print("Cancelled download")
break
except Exception:
errors += 1
print(f"Failed to download {i+1} of {T}: {file_name}")
print(
f"{done} Downloads completed, {errors} downloads failed, {T} files available"
)
return
def find_tiles(self, region=None, return_type="list", geopandas_args={}, API={}):
"""
Find a list of downloadable files.
Args:
region (str | list, optional): An URL|filepath to a vector dataset Or a list of bounds in the form of [minx, miny, maxx, maxy].
Alternatively you could use API parameters such as polygon or bbox.
out_dir (str, optional): The directory to download the files to. Defaults to None, which uses the current working directory.
return_type (str): list | dict. Defaults to list. Changes the return output type and content.
geopandas_args (dict, optional): A dictionary of arguments to pass to the geopandas.read_file() function.
Used for reading a region URL|filepath.
API (dict, optional): A dictionary of arguments to pass to the self.find_details() function.
Exposes most of the documented API parameters. Defaults to {}.
Returns:
list: A list of download_urls.
dict: A dictionary with urls and related metadata
"""
assert region or API, "Provide a region or use the API"
if region:
API["bbox"] = self.parse_region(region, geopandas_args)
results = self.find_details(**API)
if return_type == "list":
return [i["downloadURL"] for i in results.get("items")]
return results
def find_details(
self,
bbox: List[float] = None,
polygon: List[Tuple[float, float]] = None,
datasets: str = None,
prodFormats: str = None,
prodExtents: str = None,
q: str = None,
dateType: str = None,
start: str = None,
end: str = None,
offset: int = 0,
max: int = None,
outputFormat: str = "JSON",
polyType: str = None,
polyCode: str = None,
extentQuery: int = None,
) -> Dict:
"""
Possible search parameters (kwargs) support by API
Parameter Values
Description
---------------------------------------------------------------------------------------------------
bbox 'minx, miny, maxx, maxy'
Geographic longitude/latitude values expressed in decimal degrees in a comma-delimited list.
polygon '[x,y x,y x,y x,y x,y]'
Polygon, longitude/latitude values expressed in decimal degrees in a space-delimited list.
datasets See: Datasets (Optional)
Dataset tag name (sbDatasetTag)
From https://apps.nationalmap.gov/tnmaccess/#/product
prodFormats See: Product Formats (Optional)
Dataset-specific format
prodExtents See: Product Extents (Optional)
Dataset-specific extent
q free text
Text input which can be used to filter by product titles and text descriptions.
dateType dateCreated | lastUpdated | Publication
Type of date to search by.
start 'YYYY-MM-DD'
Start date
end 'YYYY-MM-DD'
End date (required if start date is provided)
offset integer
Offset into paginated results - default=0
max integer
Number of results returned
outputFormat JSON | CSV | pjson
Default=JSON
polyType state | huc2 | huc4 | huc8
Well Known Polygon Type. Use this parameter to deliver data by state or HUC
(hydrologic unit codes defined by the Watershed Boundary Dataset/WBD)
polyCode state FIPS code or huc number
Well Known Polygon Code. This value needs to coordinate with the polyType parameter.
extentQuery integer
A Polygon code in the science base system, typically from an uploaded shapefile
"""
try:
# call locals before creating new locals
used_locals = {k: v for k, v in locals().items() if v and k != "self"}
# Parsing
if polygon:
used_locals["polygon"] = ",".join(
" ".join(map(str, point)) for point in polygon
)
if bbox:
used_locals["bbox"] = str(bbox)[1:-1]
if max:
max += 2
# Fetch response
response = requests.get(f"{self.api_endpoint}products?", params=used_locals)
if response.status_code // 100 == 2:
return response.json()
else:
# Parameter validation handled by API endpoint error responses
print(response.json())
return {}
except Exception as e:
print(e)
return {}
datasets: list
property
readonly
¶
Returns a list of dataset tags (most common human readable self description for specific datasets).
datasets_full: list
property
readonly
¶
Full description of datasets provided. Returns a JSON or empty list.
prodFormats: list
property
readonly
¶
Return all datatypes available in any of the collections. Note that "All" is only peculiar to one dataset.
download_tiles(self, region=None, out_dir=None, download_args={}, geopandas_args={}, API={})
¶
Download the US National Elevation Datasets (NED) for a region.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
region |
str | list |
An URL|filepath to a vector dataset Or a list of bounds in the form of [minx, miny, maxx, maxy]. Alternatively you could use API parameters such as polygon or bbox. |
None |
out_dir |
str |
The directory to download the files to. Defaults to None, which uses the current working directory. |
None |
download_args |
dict |
A dictionary of arguments to pass to the download_file function. Defaults to {}. |
{} |
geopandas_args |
dict |
A dictionary of arguments to pass to the geopandas.read_file() function. Used for reading a region URL|filepath. |
{} |
API |
dict |
A dictionary of arguments to pass to the self.find_details() function. Exposes most of the documented API. Defaults to {}. |
{} |
Returns:
Type | Description |
---|---|
None |
None |
Source code in leafmap/common.py
def download_tiles(
self, region=None, out_dir=None, download_args={}, geopandas_args={}, API={}
) -> None:
"""
Download the US National Elevation Datasets (NED) for a region.
Args:
region (str | list, optional): An URL|filepath to a vector dataset Or a list of bounds in the form of [minx, miny, maxx, maxy].
Alternatively you could use API parameters such as polygon or bbox.
out_dir (str, optional): The directory to download the files to. Defaults to None, which uses the current working directory.
download_args (dict, optional): A dictionary of arguments to pass to the download_file function. Defaults to {}.
geopandas_args (dict, optional): A dictionary of arguments to pass to the geopandas.read_file() function.
Used for reading a region URL|filepath.
API (dict, optional): A dictionary of arguments to pass to the self.find_details() function.
Exposes most of the documented API. Defaults to {}.
Returns:
None
"""
if os.environ.get("USE_MKDOCS") is not None:
return
if out_dir is None:
out_dir = os.getcwd()
else:
out_dir = os.path.abspath(out_dir)
tiles = self.find_tiles(
region, return_type="list", geopandas_args=geopandas_args, API=API
)
T = len(tiles)
errors = 0
done = 0
for i, link in enumerate(tiles):
file_name = os.path.basename(link)
out_name = os.path.join(out_dir, file_name)
if i < 5 or (i < 50 and not (i % 5)) or not (i % 20):
print(f"Downloading {i+1} of {T}: {file_name}")
try:
download_file(link, out_name, **download_args)
done += 1
except KeyboardInterrupt:
print("Cancelled download")
break
except Exception:
errors += 1
print(f"Failed to download {i+1} of {T}: {file_name}")
print(
f"{done} Downloads completed, {errors} downloads failed, {T} files available"
)
return
find_details(self, bbox=None, polygon=None, datasets=None, prodFormats=None, prodExtents=None, q=None, dateType=None, start=None, end=None, offset=0, max=None, outputFormat='JSON', polyType=None, polyCode=None, extentQuery=None)
¶
Possible search parameters (kwargs) support by API
Parameter Values Description
bbox 'minx, miny, maxx, maxy' Geographic longitude/latitude values expressed in decimal degrees in a comma-delimited list. polygon '[x,y x,y x,y x,y x,y]' Polygon, longitude/latitude values expressed in decimal degrees in a space-delimited list. datasets See: Datasets (Optional) Dataset tag name (sbDatasetTag) From https://apps.nationalmap.gov/tnmaccess/#/product prodFormats See: Product Formats (Optional) Dataset-specific format
prodExtents See: Product Extents (Optional) Dataset-specific extent q free text Text input which can be used to filter by product titles and text descriptions. dateType dateCreated | lastUpdated | Publication Type of date to search by. start 'YYYY-MM-DD' Start date end 'YYYY-MM-DD' End date (required if start date is provided) offset integer Offset into paginated results - default=0 max integer Number of results returned outputFormat JSON | CSV | pjson Default=JSON polyType state | huc2 | huc4 | huc8 Well Known Polygon Type. Use this parameter to deliver data by state or HUC (hydrologic unit codes defined by the Watershed Boundary Dataset/WBD) polyCode state FIPS code or huc number Well Known Polygon Code. This value needs to coordinate with the polyType parameter. extentQuery integer A Polygon code in the science base system, typically from an uploaded shapefile
Source code in leafmap/common.py
def find_details(
self,
bbox: List[float] = None,
polygon: List[Tuple[float, float]] = None,
datasets: str = None,
prodFormats: str = None,
prodExtents: str = None,
q: str = None,
dateType: str = None,
start: str = None,
end: str = None,
offset: int = 0,
max: int = None,
outputFormat: str = "JSON",
polyType: str = None,
polyCode: str = None,
extentQuery: int = None,
) -> Dict:
"""
Possible search parameters (kwargs) support by API
Parameter Values
Description
---------------------------------------------------------------------------------------------------
bbox 'minx, miny, maxx, maxy'
Geographic longitude/latitude values expressed in decimal degrees in a comma-delimited list.
polygon '[x,y x,y x,y x,y x,y]'
Polygon, longitude/latitude values expressed in decimal degrees in a space-delimited list.
datasets See: Datasets (Optional)
Dataset tag name (sbDatasetTag)
From https://apps.nationalmap.gov/tnmaccess/#/product
prodFormats See: Product Formats (Optional)
Dataset-specific format
prodExtents See: Product Extents (Optional)
Dataset-specific extent
q free text
Text input which can be used to filter by product titles and text descriptions.
dateType dateCreated | lastUpdated | Publication
Type of date to search by.
start 'YYYY-MM-DD'
Start date
end 'YYYY-MM-DD'
End date (required if start date is provided)
offset integer
Offset into paginated results - default=0
max integer
Number of results returned
outputFormat JSON | CSV | pjson
Default=JSON
polyType state | huc2 | huc4 | huc8
Well Known Polygon Type. Use this parameter to deliver data by state or HUC
(hydrologic unit codes defined by the Watershed Boundary Dataset/WBD)
polyCode state FIPS code or huc number
Well Known Polygon Code. This value needs to coordinate with the polyType parameter.
extentQuery integer
A Polygon code in the science base system, typically from an uploaded shapefile
"""
try:
# call locals before creating new locals
used_locals = {k: v for k, v in locals().items() if v and k != "self"}
# Parsing
if polygon:
used_locals["polygon"] = ",".join(
" ".join(map(str, point)) for point in polygon
)
if bbox:
used_locals["bbox"] = str(bbox)[1:-1]
if max:
max += 2
# Fetch response
response = requests.get(f"{self.api_endpoint}products?", params=used_locals)
if response.status_code // 100 == 2:
return response.json()
else:
# Parameter validation handled by API endpoint error responses
print(response.json())
return {}
except Exception as e:
print(e)
return {}
find_tiles(self, region=None, return_type='list', geopandas_args={}, API={})
¶
Find a list of downloadable files.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
region |
str | list |
An URL|filepath to a vector dataset Or a list of bounds in the form of [minx, miny, maxx, maxy]. Alternatively you could use API parameters such as polygon or bbox. |
None |
out_dir |
str |
The directory to download the files to. Defaults to None, which uses the current working directory. |
required |
return_type |
str |
list | dict. Defaults to list. Changes the return output type and content. |
'list' |
geopandas_args |
dict |
A dictionary of arguments to pass to the geopandas.read_file() function. Used for reading a region URL|filepath. |
{} |
API |
dict |
A dictionary of arguments to pass to the self.find_details() function. Exposes most of the documented API parameters. Defaults to {}. |
{} |
Returns:
Type | Description |
---|---|
list |
A list of download_urls. dict: A dictionary with urls and related metadata |
Source code in leafmap/common.py
def find_tiles(self, region=None, return_type="list", geopandas_args={}, API={}):
"""
Find a list of downloadable files.
Args:
region (str | list, optional): An URL|filepath to a vector dataset Or a list of bounds in the form of [minx, miny, maxx, maxy].
Alternatively you could use API parameters such as polygon or bbox.
out_dir (str, optional): The directory to download the files to. Defaults to None, which uses the current working directory.
return_type (str): list | dict. Defaults to list. Changes the return output type and content.
geopandas_args (dict, optional): A dictionary of arguments to pass to the geopandas.read_file() function.
Used for reading a region URL|filepath.
API (dict, optional): A dictionary of arguments to pass to the self.find_details() function.
Exposes most of the documented API parameters. Defaults to {}.
Returns:
list: A list of download_urls.
dict: A dictionary with urls and related metadata
"""
assert region or API, "Provide a region or use the API"
if region:
API["bbox"] = self.parse_region(region, geopandas_args)
results = self.find_details(**API)
if return_type == "list":
return [i["downloadURL"] for i in results.get("items")]
return results
parse_region(self, region, geopandas_args={})
¶
Translate a Vector dataset to its bounding box.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
region |
str | list |
an URL|filepath to a vector dataset to a polygon |
required |
geopandas_reader_args |
dict |
A dictionary of arguments to pass to the geopandas.read_file() function. Used for reading a region URL|filepath. |
required |
Source code in leafmap/common.py
def parse_region(self, region, geopandas_args={}) -> list:
"""
Translate a Vector dataset to its bounding box.
Args:
region (str | list): an URL|filepath to a vector dataset to a polygon
geopandas_reader_args (dict, optional): A dictionary of arguments to pass to the geopandas.read_file() function.
Used for reading a region URL|filepath.
"""
import geopandas as gpd
if isinstance(region, str):
if region.startswith("http"):
region = github_raw_url(region)
region = download_file(region)
elif not os.path.exists(region):
raise ValueError("region must be a path or a URL to a vector dataset.")
roi = gpd.read_file(region, **geopandas_args)
roi = roi.to_crs(epsg=4326)
return roi.total_bounds
return region
WhiteboxTools (WhiteboxTools)
¶
This class inherits the whitebox WhiteboxTools class.
Source code in leafmap/common.py
class WhiteboxTools(whitebox.WhiteboxTools):
"""This class inherits the whitebox WhiteboxTools class."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
__install_from_github(url)
¶
Install a package from a GitHub repository.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
The URL of the GitHub repository. |
required |
Source code in leafmap/common.py
def __install_from_github(url: str) -> None:
"""Install a package from a GitHub repository.
Args:
url (str): The URL of the GitHub repository.
"""
try:
download_dir = os.path.join(os.path.expanduser("~"), "Downloads")
if not os.path.exists(download_dir):
os.makedirs(download_dir)
repo_name = os.path.basename(url)
zip_url = os.path.join(url, "archive/master.zip")
filename = repo_name + "-master.zip"
download_from_url(
url=zip_url, out_file_name=filename, out_dir=download_dir, unzip=True
)
pkg_dir = os.path.join(download_dir, repo_name + "-master")
pkg_name = os.path.basename(url)
work_dir = os.getcwd()
os.chdir(pkg_dir)
print("Installing {}...".format(pkg_name))
cmd = "pip install ."
os.system(cmd)
os.chdir(work_dir)
print("{} has been installed successfully.".format(pkg_name))
# print("\nPlease comment out 'install_from_github()' and restart the kernel to take effect:\nJupyter menu -> Kernel -> Restart & Clear Output")
except Exception as e:
raise Exception(e)
add_crs(filename, epsg)
¶
Add a CRS to a raster dataset.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filename |
str |
The filename of the raster dataset. |
required |
epsg |
int | str |
The EPSG code of the CRS. |
required |
Source code in leafmap/common.py
def add_crs(filename, epsg):
"""Add a CRS to a raster dataset.
Args:
filename (str): The filename of the raster dataset.
epsg (int | str): The EPSG code of the CRS.
"""
try:
import rasterio
except ImportError:
raise ImportError(
"rasterio is required for adding a CRS to a raster. Please install it using 'pip install rasterio'."
)
if not os.path.exists(filename):
raise ValueError("filename must exist.")
if isinstance(epsg, int):
epsg = f"EPSG:{epsg}"
elif isinstance(epsg, str):
epsg = "EPSG:" + epsg
else:
raise ValueError("epsg must be an integer or string.")
crs = rasterio.crs.CRS({"init": epsg})
with rasterio.open(filename, mode="r+") as src:
src.crs = crs
add_image_to_gif(in_gif, out_gif, in_image, xy=None, image_size=(80, 80), circle_mask=False)
¶
Adds an image logo to a GIF image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_gif |
str |
Input file path to the GIF image. |
required |
out_gif |
str |
Output file path to the GIF image. |
required |
in_image |
str |
Input file path to the image. |
required |
xy |
tuple |
Top left corner of the text. It can be formatted like this: (10, 10) or ('15%', '25%'). Defaults to None. |
None |
image_size |
tuple |
Resize image. Defaults to (80, 80). |
(80, 80) |
circle_mask |
bool |
Whether to apply a circle mask to the image. This only works with non-png images. Defaults to False. |
False |
Source code in leafmap/common.py
def add_image_to_gif(
in_gif, out_gif, in_image, xy=None, image_size=(80, 80), circle_mask=False
):
"""Adds an image logo to a GIF image.
Args:
in_gif (str): Input file path to the GIF image.
out_gif (str): Output file path to the GIF image.
in_image (str): Input file path to the image.
xy (tuple, optional): Top left corner of the text. It can be formatted like this: (10, 10) or ('15%', '25%'). Defaults to None.
image_size (tuple, optional): Resize image. Defaults to (80, 80).
circle_mask (bool, optional): Whether to apply a circle mask to the image. This only works with non-png images. Defaults to False.
"""
import io
from PIL import Image, ImageDraw, ImageSequence
warnings.simplefilter("ignore")
in_gif = os.path.abspath(in_gif)
is_url = False
if in_image.startswith("http"):
is_url = True
if not os.path.exists(in_gif):
print("The input gif file does not exist.")
return
if (not is_url) and (not os.path.exists(in_image)):
print("The provided logo file does not exist.")
return
out_dir = check_dir((os.path.dirname(out_gif)))
if not os.path.exists(out_dir):
os.makedirs(out_dir)
try:
gif = Image.open(in_gif)
except Exception as e:
print("An error occurred while opening the image.")
print(e)
return
logo_raw_image = None
try:
if in_image.startswith("http"):
logo_raw_image = open_image_from_url(in_image)
else:
in_image = os.path.abspath(in_image)
logo_raw_image = Image.open(in_image)
except Exception as e:
print(e)
logo_raw_size = logo_raw_image.size
ratio = max(
logo_raw_size[0] / image_size[0],
logo_raw_size[1] / image_size[1],
)
image_resize = (int(logo_raw_size[0] / ratio), int(logo_raw_size[1] / ratio))
image_size = min(logo_raw_size[0], image_size[0]), min(
logo_raw_size[1], image_size[1]
)
logo_image = logo_raw_image.convert("RGBA")
logo_image.thumbnail(image_size, Image.ANTIALIAS)
gif_width, gif_height = gif.size
mask_im = None
if circle_mask:
mask_im = Image.new("L", image_size, 0)
draw = ImageDraw.Draw(mask_im)
draw.ellipse((0, 0, image_size[0], image_size[1]), fill=255)
if has_transparency(logo_raw_image):
mask_im = logo_image.copy()
if xy is None:
# default logo location is 5% width and 5% height of the image.
delta = 10
xy = (gif_width - image_resize[0] - delta, gif_height - image_resize[1] - delta)
# xy = (int(0.05 * gif_width), int(0.05 * gif_height))
elif (xy is not None) and (not isinstance(xy, tuple)) and (len(xy) == 2):
print("xy must be a tuple, e.g., (10, 10), ('10%', '10%')")
return
elif all(isinstance(item, int) for item in xy) and (len(xy) == 2):
x, y = xy
if (x > 0) and (x < gif_width) and (y > 0) and (y < gif_height):
pass
else:
print(
"xy is out of bounds. x must be within [0, {}], and y must be within [0, {}]".format(
gif_width, gif_height
)
)
return
elif all(isinstance(item, str) for item in xy) and (len(xy) == 2):
x, y = xy
if ("%" in x) and ("%" in y):
try:
x = int(float(x.replace("%", "")) / 100.0 * gif_width)
y = int(float(y.replace("%", "")) / 100.0 * gif_height)
xy = (x, y)
except Exception:
raise Exception(
"The specified xy is invalid. It must be formatted like this ('10%', '10%')"
)
else:
raise Exception(
"The specified xy is invalid. It must be formatted like this: (10, 10) or ('10%', '10%')"
)
try:
frames = []
for _, frame in enumerate(ImageSequence.Iterator(gif)):
frame = frame.convert("RGBA")
frame.paste(logo_image, xy, mask_im)
b = io.BytesIO()
frame.save(b, format="GIF")
frame = Image.open(b)
frames.append(frame)
frames[0].save(out_gif, save_all=True, append_images=frames[1:])
except Exception as e:
print(e)
add_mask_to_image(image, mask, output, color='red')
¶
Overlay a binary mask (e.g., roads, building footprints, etc) on an image. Credits to Xingjian Shi for the sample code.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
str |
A local path or HTTP URL to an image. |
required |
mask |
str |
A local path or HTTP URL to a binary mask. |
required |
output |
str |
A local path to the output image. |
required |
color |
str |
Color of the mask. Defaults to 'red'. |
'red' |
Exceptions:
Type | Description |
---|---|
ImportError |
If rasterio and detectron2 are not installed. |
Source code in leafmap/common.py
def add_mask_to_image(image, mask, output, color="red"):
"""Overlay a binary mask (e.g., roads, building footprints, etc) on an image. Credits to Xingjian Shi for the sample code.
Args:
image (str): A local path or HTTP URL to an image.
mask (str): A local path or HTTP URL to a binary mask.
output (str): A local path to the output image.
color (str, optional): Color of the mask. Defaults to 'red'.
Raises:
ImportError: If rasterio and detectron2 are not installed.
"""
try:
import rasterio
from detectron2.utils.visualizer import Visualizer
from PIL import Image
except ImportError:
raise ImportError(
"Please install rasterio and detectron2 to use this function. See https://detectron2.readthedocs.io/en/latest/tutorials/install.html"
)
ds = rasterio.open(image)
image_arr = ds.read()
mask_arr = rasterio.open(mask).read()
vis = Visualizer(image_arr.transpose((1, 2, 0)))
vis.draw_binary_mask(mask_arr[0] > 0, color=color)
out_arr = Image.fromarray(vis.get_output().get_image())
out_arr.save(output)
if ds.crs is not None:
numpy_to_cog(output, output, profile=image)
add_progress_bar_to_gif(in_gif, out_gif, progress_bar_color='blue', progress_bar_height=5, duration=100, loop=0)
¶
Adds a progress bar to a GIF image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_gif |
str |
The file path to the input GIF image. |
required |
out_gif |
str |
The file path to the output GIF image. |
required |
progress_bar_color |
str |
Color for the progress bar. Defaults to 'white'. |
'blue' |
progress_bar_height |
int |
Height of the progress bar. Defaults to 5. |
5 |
duration |
int |
controls how long each frame will be displayed for, in milliseconds. It is the inverse of the frame rate. Setting it to 100 milliseconds gives 10 frames per second. You can decrease the duration to give a smoother animation. Defaults to 100. |
100 |
loop |
int |
controls how many times the animation repeats. The default, 1, means that the animation will play once and then stop (displaying the last frame). A value of 0 means that the animation will repeat forever. Defaults to 0. |
0 |
Source code in leafmap/common.py
def add_progress_bar_to_gif(
in_gif,
out_gif,
progress_bar_color="blue",
progress_bar_height=5,
duration=100,
loop=0,
):
"""Adds a progress bar to a GIF image.
Args:
in_gif (str): The file path to the input GIF image.
out_gif (str): The file path to the output GIF image.
progress_bar_color (str, optional): Color for the progress bar. Defaults to 'white'.
progress_bar_height (int, optional): Height of the progress bar. Defaults to 5.
duration (int, optional): controls how long each frame will be displayed for, in milliseconds. It is the inverse of the frame rate. Setting it to 100 milliseconds gives 10 frames per second. You can decrease the duration to give a smoother animation. Defaults to 100.
loop (int, optional): controls how many times the animation repeats. The default, 1, means that the animation will play once and then stop (displaying the last frame). A value of 0 means that the animation will repeat forever. Defaults to 0.
"""
import io
from PIL import Image, ImageDraw, ImageSequence
warnings.simplefilter("ignore")
in_gif = os.path.abspath(in_gif)
out_gif = os.path.abspath(out_gif)
if not os.path.exists(in_gif):
print("The input gif file does not exist.")
return
if not os.path.exists(os.path.dirname(out_gif)):
os.makedirs(os.path.dirname(out_gif))
progress_bar_color = check_color(progress_bar_color)
try:
image = Image.open(in_gif)
except Exception as e:
raise Exception("An error occurred while opening the gif.")
count = image.n_frames
W, H = image.size
progress_bar_widths = [i * 1.0 / count * W for i in range(1, count + 1)]
progress_bar_shapes = [
[(0, H - progress_bar_height), (x, H)] for x in progress_bar_widths
]
try:
frames = []
# Loop over each frame in the animated image
for index, frame in enumerate(ImageSequence.Iterator(image)):
# Draw the text on the frame
frame = frame.convert("RGB")
draw = ImageDraw.Draw(frame)
# w, h = draw.textsize(text[index])
draw.rectangle(progress_bar_shapes[index], fill=progress_bar_color)
del draw
b = io.BytesIO()
frame.save(b, format="GIF")
frame = Image.open(b)
frames.append(frame)
# https://www.pythoninformer.com/python-libraries/pillow/creating-animated-gif/
# Save the frames as a new image
frames[0].save(
out_gif,
save_all=True,
append_images=frames[1:],
duration=duration,
loop=loop,
optimize=True,
)
except Exception as e:
raise Exception(e)
add_text_to_gif(in_gif, out_gif, xy=None, text_sequence=None, font_type='arial.ttf', font_size=20, font_color='#000000', add_progress_bar=True, progress_bar_color='white', progress_bar_height=5, duration=100, loop=0)
¶
Adds animated text to a GIF image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_gif |
str |
The file path to the input GIF image. |
required |
out_gif |
str |
The file path to the output GIF image. |
required |
xy |
tuple |
Top left corner of the text. It can be formatted like this: (10, 10) or ('15%', '25%'). Defaults to None. |
None |
text_sequence |
int, str, list |
Text to be drawn. It can be an integer number, a string, or a list of strings. Defaults to None. |
None |
font_type |
str |
Font type. Defaults to "arial.ttf". |
'arial.ttf' |
font_size |
int |
Font size. Defaults to 20. |
20 |
font_color |
str |
Font color. It can be a string (e.g., 'red'), rgb tuple (e.g., (255, 127, 0)), or hex code (e.g., '#ff00ff'). Defaults to '#000000'. |
'#000000' |
add_progress_bar |
bool |
Whether to add a progress bar at the bottom of the GIF. Defaults to True. |
True |
progress_bar_color |
str |
Color for the progress bar. Defaults to 'white'. |
'white' |
progress_bar_height |
int |
Height of the progress bar. Defaults to 5. |
5 |
duration |
int |
controls how long each frame will be displayed for, in milliseconds. It is the inverse of the frame rate. Setting it to 100 milliseconds gives 10 frames per second. You can decrease the duration to give a smoother animation.. Defaults to 100. |
100 |
loop |
int |
controls how many times the animation repeats. The default, 1, means that the animation will play once and then stop (displaying the last frame). A value of 0 means that the animation will repeat forever. Defaults to 0. |
0 |
Source code in leafmap/common.py
def add_text_to_gif(
in_gif,
out_gif,
xy=None,
text_sequence=None,
font_type="arial.ttf",
font_size=20,
font_color="#000000",
add_progress_bar=True,
progress_bar_color="white",
progress_bar_height=5,
duration=100,
loop=0,
):
"""Adds animated text to a GIF image.
Args:
in_gif (str): The file path to the input GIF image.
out_gif (str): The file path to the output GIF image.
xy (tuple, optional): Top left corner of the text. It can be formatted like this: (10, 10) or ('15%', '25%'). Defaults to None.
text_sequence (int, str, list, optional): Text to be drawn. It can be an integer number, a string, or a list of strings. Defaults to None.
font_type (str, optional): Font type. Defaults to "arial.ttf".
font_size (int, optional): Font size. Defaults to 20.
font_color (str, optional): Font color. It can be a string (e.g., 'red'), rgb tuple (e.g., (255, 127, 0)), or hex code (e.g., '#ff00ff'). Defaults to '#000000'.
add_progress_bar (bool, optional): Whether to add a progress bar at the bottom of the GIF. Defaults to True.
progress_bar_color (str, optional): Color for the progress bar. Defaults to 'white'.
progress_bar_height (int, optional): Height of the progress bar. Defaults to 5.
duration (int, optional): controls how long each frame will be displayed for, in milliseconds. It is the inverse of the frame rate. Setting it to 100 milliseconds gives 10 frames per second. You can decrease the duration to give a smoother animation.. Defaults to 100.
loop (int, optional): controls how many times the animation repeats. The default, 1, means that the animation will play once and then stop (displaying the last frame). A value of 0 means that the animation will repeat forever. Defaults to 0.
"""
import io
import pkg_resources
from PIL import Image, ImageDraw, ImageFont, ImageSequence
warnings.simplefilter("ignore")
pkg_dir = os.path.dirname(pkg_resources.resource_filename("leafmap", "leafmap.py"))
default_font = os.path.join(pkg_dir, "data/fonts/arial.ttf")
in_gif = os.path.abspath(in_gif)
out_gif = os.path.abspath(out_gif)
if not os.path.exists(in_gif):
print("The input gif file does not exist.")
return
if not os.path.exists(os.path.dirname(out_gif)):
os.makedirs(os.path.dirname(out_gif))
if font_type == "arial.ttf":
font = ImageFont.truetype(default_font, font_size)
elif font_type == "alibaba.otf":
default_font = os.path.join(pkg_dir, "data/fonts/alibaba.otf")
font = ImageFont.truetype(default_font, font_size)
else:
try:
font_list = system_fonts(show_full_path=True)
font_names = [os.path.basename(f) for f in font_list]
if (font_type in font_list) or (font_type in font_names):
font = ImageFont.truetype(font_type, font_size)
else:
print(
"The specified font type could not be found on your system. Using the default font instead."
)
font = ImageFont.truetype(default_font, font_size)
except Exception as e:
print(e)
font = ImageFont.truetype(default_font, font_size)
color = check_color(font_color)
progress_bar_color = check_color(progress_bar_color)
try:
image = Image.open(in_gif)
except Exception as e:
print("An error occurred while opening the gif.")
print(e)
return
count = image.n_frames
W, H = image.size
progress_bar_widths = [i * 1.0 / count * W for i in range(1, count + 1)]
progress_bar_shapes = [
[(0, H - progress_bar_height), (x, H)] for x in progress_bar_widths
]
if xy is None:
# default text location is 5% width and 5% height of the image.
xy = (int(0.05 * W), int(0.05 * H))
elif (xy is not None) and (not isinstance(xy, tuple)) and (len(xy) == 2):
print("xy must be a tuple, e.g., (10, 10), ('10%', '10%')")
return
elif all(isinstance(item, int) for item in xy) and (len(xy) == 2):
x, y = xy
if (x > 0) and (x < W) and (y > 0) and (y < H):
pass
else:
print(
f"xy is out of bounds. x must be within [0, {W}], and y must be within [0, {H}]"
)
return
elif all(isinstance(item, str) for item in xy) and (len(xy) == 2):
x, y = xy
if ("%" in x) and ("%" in y):
try:
x = int(float(x.replace("%", "")) / 100.0 * W)
y = int(float(y.replace("%", "")) / 100.0 * H)
xy = (x, y)
except Exception:
raise Exception(
"The specified xy is invalid. It must be formatted like this ('10%', '10%')"
)
else:
print(
"The specified xy is invalid. It must be formatted like this: (10, 10) or ('10%', '10%')"
)
return
if text_sequence is None:
text = [str(x) for x in range(1, count + 1)]
elif isinstance(text_sequence, int):
text = [str(x) for x in range(text_sequence, text_sequence + count + 1)]
elif isinstance(text_sequence, str):
try:
text_sequence = int(text_sequence)
text = [str(x) for x in range(text_sequence, text_sequence + count + 1)]
except Exception:
text = [text_sequence] * count
elif isinstance(text_sequence, list) and len(text_sequence) != count:
print(
f"The length of the text sequence must be equal to the number ({count}) of frames in the gif."
)
return
else:
text = [str(x) for x in text_sequence]
try:
frames = []
# Loop over each frame in the animated image
for index, frame in enumerate(ImageSequence.Iterator(image)):
# Draw the text on the frame
frame = frame.convert("RGB")
draw = ImageDraw.Draw(frame)
# w, h = draw.textsize(text[index])
draw.text(xy, text[index], font=font, fill=color)
if add_progress_bar:
draw.rectangle(progress_bar_shapes[index], fill=progress_bar_color)
del draw
b = io.BytesIO()
frame.save(b, format="GIF")
frame = Image.open(b)
frames.append(frame)
# https://www.pythoninformer.com/python-libraries/pillow/creating-animated-gif/
# Save the frames as a new image
frames[0].save(
out_gif,
save_all=True,
append_images=frames[1:],
duration=duration,
loop=loop,
optimize=True,
)
except Exception as e:
print(e)
adjust_longitude(in_fc)
¶
Adjusts longitude if it is less than -180 or greater than 180.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_fc |
dict |
The input dictionary containing coordinates. |
required |
Returns:
Type | Description |
---|---|
dict |
A dictionary containing the converted longitudes |
Source code in leafmap/common.py
def adjust_longitude(in_fc):
"""Adjusts longitude if it is less than -180 or greater than 180.
Args:
in_fc (dict): The input dictionary containing coordinates.
Returns:
dict: A dictionary containing the converted longitudes
"""
try:
keys = in_fc.keys()
if "geometry" in keys:
coordinates = in_fc["geometry"]["coordinates"]
if in_fc["geometry"]["type"] == "Point":
longitude = coordinates[0]
if longitude < -180:
longitude = 360 + longitude
elif longitude > 180:
longitude = longitude - 360
in_fc["geometry"]["coordinates"][0] = longitude
elif in_fc["geometry"]["type"] == "Polygon":
for index1, item in enumerate(coordinates):
for index2, element in enumerate(item):
longitude = element[0]
if longitude < -180:
longitude = 360 + longitude
elif longitude > 180:
longitude = longitude - 360
in_fc["geometry"]["coordinates"][index1][index2][0] = longitude
elif in_fc["geometry"]["type"] == "LineString":
for index, element in enumerate(coordinates):
longitude = element[0]
if longitude < -180:
longitude = 360 + longitude
elif longitude > 180:
longitude = longitude - 360
in_fc["geometry"]["coordinates"][index][0] = longitude
elif "type" in keys:
coordinates = in_fc["coordinates"]
if in_fc["type"] == "Point":
longitude = coordinates[0]
if longitude < -180:
longitude = 360 + longitude
elif longitude > 180:
longitude = longitude - 360
in_fc["coordinates"][0] = longitude
elif in_fc["type"] == "Polygon":
for index1, item in enumerate(coordinates):
for index2, element in enumerate(item):
longitude = element[0]
if longitude < -180:
longitude = 360 + longitude
elif longitude > 180:
longitude = longitude - 360
in_fc["coordinates"][index1][index2][0] = longitude
elif in_fc["type"] == "LineString":
for index, element in enumerate(coordinates):
longitude = element[0]
if longitude < -180:
longitude = 360 + longitude
elif longitude > 180:
longitude = longitude - 360
in_fc["coordinates"][index][0] = longitude
return in_fc
except Exception as e:
print(e)
return None
arc_active_map()
¶
Get the active map in ArcGIS Pro.
Returns:
Type | Description |
---|---|
arcpy.Map |
The active map in ArcGIS Pro. |
Source code in leafmap/common.py
def arc_active_map():
"""Get the active map in ArcGIS Pro.
Returns:
arcpy.Map: The active map in ArcGIS Pro.
"""
if is_arcpy():
import arcpy # pylint: disable=E0401
aprx = arcpy.mp.ArcGISProject("CURRENT")
m = aprx.activeMap
return m
else:
return None
arc_active_view()
¶
Get the active view in ArcGIS Pro.
Returns:
Type | Description |
---|---|
arcpy.MapView |
The active view in ArcGIS Pro. |
Source code in leafmap/common.py
def arc_active_view():
"""Get the active view in ArcGIS Pro.
Returns:
arcpy.MapView: The active view in ArcGIS Pro.
"""
if is_arcpy():
import arcpy # pylint: disable=E0401
aprx = arcpy.mp.ArcGISProject("CURRENT")
view = aprx.activeView
return view
else:
return None
arc_add_layer(url, name=None, shown=True, opacity=1.0)
¶
Add a layer to the active map in ArcGIS Pro.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
The URL of the tile layer to add. |
required |
name |
str |
The name of the layer. Defaults to None. |
None |
shown |
bool |
Whether the layer is shown. Defaults to True. |
True |
opacity |
float |
The opacity of the layer. Defaults to 1.0. |
1.0 |
Source code in leafmap/common.py
def arc_add_layer(url, name=None, shown=True, opacity=1.0):
"""Add a layer to the active map in ArcGIS Pro.
Args:
url (str): The URL of the tile layer to add.
name (str, optional): The name of the layer. Defaults to None.
shown (bool, optional): Whether the layer is shown. Defaults to True.
opacity (float, optional): The opacity of the layer. Defaults to 1.0.
"""
if is_arcpy():
m = arc_active_map()
if m is not None:
m.addDataFromPath(url)
if isinstance(name, str):
layers = m.listLayers("Tiled service layer")
if len(layers) > 0:
layer = layers[0]
layer.name = name
layer.visible = shown
layer.transparency = 100 - (opacity * 100)
arc_zoom_to_bounds(bounds)
¶
Zoom to a bounding box.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
bounds |
list |
The bounding box to zoom to in the form [xmin, ymin, xmax, ymax] or [(ymin, xmin), (ymax, xmax)]. |
required |
Exceptions:
Type | Description |
---|---|
ValueError |
description |
Source code in leafmap/common.py
def arc_zoom_to_bounds(bounds):
"""Zoom to a bounding box.
Args:
bounds (list): The bounding box to zoom to in the form [xmin, ymin, xmax, ymax] or [(ymin, xmin), (ymax, xmax)].
Raises:
ValueError: _description_
"""
if len(bounds) == 4:
xmin, ymin, xmax, ymax = bounds
elif len(bounds) == 2:
(ymin, xmin), (ymax, xmax) = bounds
else:
raise ValueError("bounds must be a tuple of length 2 or 4.")
arc_zoom_to_extent(xmin, ymin, xmax, ymax)
arc_zoom_to_extent(xmin, ymin, xmax, ymax)
¶
Zoom to an extent in ArcGIS Pro.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
xmin |
float |
The minimum x value of the extent. |
required |
ymin |
float |
The minimum y value of the extent. |
required |
xmax |
float |
The maximum x value of the extent. |
required |
ymax |
float |
The maximum y value of the extent. |
required |
Source code in leafmap/common.py
def arc_zoom_to_extent(xmin, ymin, xmax, ymax):
"""Zoom to an extent in ArcGIS Pro.
Args:
xmin (float): The minimum x value of the extent.
ymin (float): The minimum y value of the extent.
xmax (float): The maximum x value of the extent.
ymax (float): The maximum y value of the extent.
"""
if is_arcpy():
import arcpy # pylint: disable=E0401
view = arc_active_view()
if view is not None:
view.camera.setExtent(
arcpy.Extent(
xmin,
ymin,
xmax,
ymax,
spatial_reference=arcpy.SpatialReference(4326),
)
)
# if isinstance(zoom, int):
# scale = 156543.04 * math.cos(0) / math.pow(2, zoom)
# view.camera.scale = scale # Not working properly
array_to_image(array, output=None, source=None, dtype=None, compress='deflate', transpose=True, cellsize=None, crs=None, transform=None, driver='COG', **kwargs)
¶
Save a NumPy array as a GeoTIFF using the projection information from an existing GeoTIFF file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
array |
np.ndarray |
The NumPy array to be saved as a GeoTIFF. |
required |
output |
str |
The path to the output image. If None, a temporary file will be created. Defaults to None. |
None |
source |
str |
The path to an existing GeoTIFF file with map projection information. Defaults to None. |
None |
dtype |
np.dtype |
The data type of the output array. Defaults to None. |
None |
compress |
str |
The compression method. Can be one of the following: "deflate", "lzw", "packbits", "jpeg". Defaults to "deflate". |
'deflate' |
transpose |
bool |
Whether to transpose the array from (bands, rows, columns) to (rows, columns, bands). Defaults to True. |
True |
cellsize |
float |
The resolution of the output image in meters. Defaults to None. |
None |
crs |
str |
The CRS of the output image. Defaults to None. |
None |
transform |
tuple |
The affine transformation matrix, can be rio.transform() or a tuple like (0.5, 0.0, -180.25, 0.0, -0.5, 83.780361). Defaults to None. |
None |
driver |
str |
The driver to use for creating the output file, such as 'GTiff'. Defaults to "COG". |
'COG' |
**kwargs |
Additional keyword arguments to be passed to the rasterio.open() function. |
{} |
Source code in leafmap/common.py
def array_to_image(
array,
output: str = None,
source: str = None,
dtype: str = None,
compress: str = "deflate",
transpose: bool = True,
cellsize: float = None,
crs: str = None,
transform: tuple = None,
driver: str = "COG",
**kwargs,
) -> str:
"""Save a NumPy array as a GeoTIFF using the projection information from an existing GeoTIFF file.
Args:
array (np.ndarray): The NumPy array to be saved as a GeoTIFF.
output (str): The path to the output image. If None, a temporary file will be created. Defaults to None.
source (str, optional): The path to an existing GeoTIFF file with map projection information. Defaults to None.
dtype (np.dtype, optional): The data type of the output array. Defaults to None.
compress (str, optional): The compression method. Can be one of the following: "deflate", "lzw", "packbits", "jpeg". Defaults to "deflate".
transpose (bool, optional): Whether to transpose the array from (bands, rows, columns) to (rows, columns, bands). Defaults to True.
cellsize (float, optional): The resolution of the output image in meters. Defaults to None.
crs (str, optional): The CRS of the output image. Defaults to None.
transform (tuple, optional): The affine transformation matrix, can be rio.transform() or a tuple like (0.5, 0.0, -180.25, 0.0, -0.5, 83.780361).
Defaults to None.
driver (str, optional): The driver to use for creating the output file, such as 'GTiff'. Defaults to "COG".
**kwargs: Additional keyword arguments to be passed to the rasterio.open() function.
"""
import numpy as np
import rasterio
import xarray as xr
import rioxarray
from rasterio.transform import Affine
if output is None:
return array_to_memory_file(
array,
source,
dtype,
compress,
transpose,
cellsize,
crs=crs,
transform=transform,
driver=driver,
**kwargs,
)
if isinstance(array, xr.DataArray):
if (
hasattr(array, "rio")
and (array.rio.crs is not None)
and (array.rio.transform() is not None)
):
if "latitude" in array.dims and "longitude" in array.dims:
array = array.rename({"latitude": "y", "longitude": "x"})
elif "lat" in array.dims and "lon" in array.dims:
array = array.rename({"lat": "y", "lon": "x"})
if array.ndim == 2 and ("x" in array.dims) and ("y" in array.dims):
array = array.transpose("y", "x")
elif array.ndim == 3 and ("x" in array.dims) and ("y" in array.dims):
dims = list(array.dims)
dims.remove("x")
dims.remove("y")
array = array.transpose(dims[0], "y", "x")
if "long_name" in array.attrs:
array.attrs.pop("long_name")
array.rio.to_raster(
output, driver=driver, compress=compress, dtype=dtype, **kwargs
)
return
if array.ndim == 3 and transpose:
array = np.transpose(array, (1, 2, 0))
out_dir = os.path.dirname(os.path.abspath(output))
if not os.path.exists(out_dir):
os.makedirs(out_dir)
if not output.endswith(".tif"):
output += ".tif"
if source is not None:
with rasterio.open(source) as src:
crs = src.crs
transform = src.transform
if compress is None:
compress = src.compression
else:
if cellsize is None:
raise ValueError("resolution must be provided if source is not provided")
if crs is None:
raise ValueError(
"crs must be provided if source is not provided, such as EPSG:3857"
)
if transform is None:
# Define the geotransformation parameters
xmin, ymin, xmax, ymax = (
0,
0,
cellsize * array.shape[1],
cellsize * array.shape[0],
)
transform = rasterio.transform.from_bounds(
xmin, ymin, xmax, ymax, array.shape[1], array.shape[0]
)
elif isinstance(transform, Affine):
pass
elif isinstance(transform, (tuple, list)):
transform = Affine(*transform)
kwargs["transform"] = transform
if dtype is None:
# Determine the minimum and maximum values in the array
min_value = np.min(array)
max_value = np.max(array)
# Determine the best dtype for the array
if min_value >= 0 and max_value <= 1:
dtype = np.float32
elif min_value >= 0 and max_value <= 255:
dtype = np.uint8
elif min_value >= -128 and max_value <= 127:
dtype = np.int8
elif min_value >= 0 and max_value <= 65535:
dtype = np.uint16
elif min_value >= -32768 and max_value <= 32767:
dtype = np.int16
else:
dtype = np.float64
# Convert the array to the best dtype
array = array.astype(dtype)
# Define the GeoTIFF metadata
metadata = {
"driver": driver,
"height": array.shape[0],
"width": array.shape[1],
"dtype": array.dtype,
"crs": crs,
"transform": transform,
}
if array.ndim == 2:
metadata["count"] = 1
elif array.ndim == 3:
metadata["count"] = array.shape[2]
if compress is not None:
metadata["compress"] = compress
metadata.update(**kwargs)
# Create a new GeoTIFF file and write the array to it
with rasterio.open(output, "w", **metadata) as dst:
if array.ndim == 2:
dst.write(array, 1)
elif array.ndim == 3:
for i in range(array.shape[2]):
dst.write(array[:, :, i], i + 1)
array_to_memory_file(array, source=None, dtype=None, compress='deflate', transpose=True, cellsize=None, crs=None, transform=None, driver='COG', **kwargs)
¶
Convert a NumPy array to a memory file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
array |
numpy.ndarray |
The input NumPy array. |
required |
source |
str |
Path to the source file to extract metadata from. Defaults to None. |
None |
dtype |
str |
The desired data type of the array. Defaults to None. |
None |
compress |
str |
The compression method for the output file. Defaults to "deflate". |
'deflate' |
transpose |
bool |
Whether to transpose the array from (bands, rows, columns) to (rows, columns, bands). Defaults to True. |
True |
cellsize |
float |
The cell size of the array if source is not provided. Defaults to None. |
None |
crs |
str |
The coordinate reference system of the array if source is not provided. Defaults to None. |
None |
transform |
tuple |
The affine transformation matrix if source is not provided. Can be rio.transform() or a tuple like (0.5, 0.0, -180.25, 0.0, -0.5, 83.780361). Defaults to None |
None |
driver |
str |
The driver to use for creating the output file, such as 'GTiff'. Defaults to "COG". |
'COG' |
**kwargs |
Additional keyword arguments to be passed to the rasterio.open() function. |
{} |
Returns:
Type | Description |
---|---|
rasterio.DatasetReader |
The rasterio dataset reader object for the converted array. |
Source code in leafmap/common.py
def array_to_memory_file(
array,
source: str = None,
dtype: str = None,
compress: str = "deflate",
transpose: bool = True,
cellsize: float = None,
crs: str = None,
transform: tuple = None,
driver="COG",
**kwargs,
):
"""Convert a NumPy array to a memory file.
Args:
array (numpy.ndarray): The input NumPy array.
source (str, optional): Path to the source file to extract metadata from. Defaults to None.
dtype (str, optional): The desired data type of the array. Defaults to None.
compress (str, optional): The compression method for the output file. Defaults to "deflate".
transpose (bool, optional): Whether to transpose the array from (bands, rows, columns) to (rows, columns, bands). Defaults to True.
cellsize (float, optional): The cell size of the array if source is not provided. Defaults to None.
crs (str, optional): The coordinate reference system of the array if source is not provided. Defaults to None.
transform (tuple, optional): The affine transformation matrix if source is not provided.
Can be rio.transform() or a tuple like (0.5, 0.0, -180.25, 0.0, -0.5, 83.780361). Defaults to None
driver (str, optional): The driver to use for creating the output file, such as 'GTiff'. Defaults to "COG".
**kwargs: Additional keyword arguments to be passed to the rasterio.open() function.
Returns:
rasterio.DatasetReader: The rasterio dataset reader object for the converted array.
"""
import rasterio
import numpy as np
import xarray as xr
from rasterio.transform import Affine
if isinstance(array, xr.DataArray):
coords = [coord for coord in array.coords]
if coords[0] == "time":
x_dim = coords[1]
y_dim = coords[2]
array = (
array.isel(time=0).rename({y_dim: "y", x_dim: "x"}).transpose("y", "x")
)
if hasattr(array, "rio"):
if hasattr(array.rio, "crs"):
crs = array.rio.crs
if transform is None and hasattr(array.rio, "transform"):
transform = array.rio.transform()
elif source is None:
if hasattr(array, "encoding"):
if "source" in array.encoding:
source = array.encoding["source"]
array = array.values
if array.ndim == 3 and transpose:
array = np.transpose(array, (1, 2, 0))
if source is not None:
with rasterio.open(source) as src:
crs = src.crs
transform = src.transform
if compress is None:
compress = src.compression
else:
if crs is None:
raise ValueError(
"crs must be provided if source is not provided, such as EPSG:3857"
)
if transform is None:
if cellsize is None:
raise ValueError("cellsize must be provided if source is not provided")
# Define the geotransformation parameters
xmin, ymin, xmax, ymax = (
0,
0,
cellsize * array.shape[1],
cellsize * array.shape[0],
)
# (west, south, east, north, width, height)
transform = rasterio.transform.from_bounds(
xmin, ymin, xmax, ymax, array.shape[1], array.shape[0]
)
elif isinstance(transform, Affine):
pass
elif isinstance(transform, (tuple, list)):
transform = Affine(*transform)
kwargs["transform"] = transform
if dtype is None:
# Determine the minimum and maximum values in the array
min_value = np.min(array)
max_value = np.max(array)
# Determine the best dtype for the array
if min_value >= 0 and max_value <= 1:
dtype = np.float32
elif min_value >= 0 and max_value <= 255:
dtype = np.uint8
elif min_value >= -128 and max_value <= 127:
dtype = np.int8
elif min_value >= 0 and max_value <= 65535:
dtype = np.uint16
elif min_value >= -32768 and max_value <= 32767:
dtype = np.int16
else:
dtype = np.float64
# Convert the array to the best dtype
array = array.astype(dtype)
# Define the GeoTIFF metadata
metadata = {
"driver": driver,
"height": array.shape[0],
"width": array.shape[1],
"dtype": array.dtype,
"crs": crs,
"transform": transform,
}
if array.ndim == 2:
metadata["count"] = 1
elif array.ndim == 3:
metadata["count"] = array.shape[2]
if compress is not None:
metadata["compress"] = compress
metadata.update(**kwargs)
# Create a new memory file and write the array to it
memory_file = rasterio.MemoryFile()
dst = memory_file.open(**metadata)
if array.ndim == 2:
dst.write(array, 1)
elif array.ndim == 3:
for i in range(array.shape[2]):
dst.write(array[:, :, i], i + 1)
dst.close()
# Read the dataset from memory
dataset_reader = rasterio.open(dst.name, mode="r")
return dataset_reader
assign_continuous_colors(df, column, cmap=None, colors=None, labels=None, scheme='Quantiles', k=5, legend_kwds=None, classification_kwds=None, to_rgb=True, return_type='array', return_legend=False)
¶
Assigns continuous colors to a DataFrame column based on a specified scheme.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
df |
A pandas DataFrame. |
required | |
column |
str |
The name of the column to assign colors. |
required |
cmap |
str |
The name of the colormap to use. |
None |
colors |
list |
A list of custom colors. |
None |
labels |
list |
A list of custom labels for the legend. |
None |
scheme |
str |
The scheme for classifying the data. Default is 'Quantiles'. |
'Quantiles' |
k |
int |
The number of classes for classification. |
5 |
legend_kwds |
dict |
Additional keyword arguments for configuring the legend. |
None |
classification_kwds |
dict |
Additional keyword arguments for configuring the classification. |
None |
to_rgb |
bool |
Whether to convert colors to RGB values. Default is True. |
True |
return_type |
str |
The type of the returned values. Default is 'array'. |
'array' |
return_legend |
bool |
Whether to return the legend. Default is False. |
False |
Returns:
Type | Description |
---|---|
Union[numpy.ndarray, Tuple[numpy.ndarray, dict]] |
The assigned colors as a numpy array or a tuple containing the colors and the legend, depending on the value of return_legend. |
Source code in leafmap/common.py
def assign_continuous_colors(
df,
column: str,
cmap: str = None,
colors: list = None,
labels: list = None,
scheme: str = "Quantiles",
k: int = 5,
legend_kwds: dict = None,
classification_kwds: dict = None,
to_rgb: bool = True,
return_type: str = "array",
return_legend: bool = False,
) -> Union[np.ndarray, Tuple[np.ndarray, dict]]:
"""Assigns continuous colors to a DataFrame column based on a specified scheme.
Args:
df: A pandas DataFrame.
column: The name of the column to assign colors.
cmap: The name of the colormap to use.
colors: A list of custom colors.
labels: A list of custom labels for the legend.
scheme: The scheme for classifying the data. Default is 'Quantiles'.
k: The number of classes for classification.
legend_kwds: Additional keyword arguments for configuring the legend.
classification_kwds: Additional keyword arguments for configuring the classification.
to_rgb: Whether to convert colors to RGB values. Default is True.
return_type: The type of the returned values. Default is 'array'.
return_legend: Whether to return the legend. Default is False.
Returns:
The assigned colors as a numpy array or a tuple containing the colors and the legend, depending on the value of return_legend.
"""
import numpy as np
data = df[[column]].copy()
new_df, legend = classify(
data, column, cmap, colors, labels, scheme, k, legend_kwds, classification_kwds
)
values = new_df["color"].values.tolist()
if to_rgb:
values = [hex_to_rgb(check_color(color)) for color in values]
if return_type == "array":
values = np.array(values, dtype=np.uint8)
if return_legend:
return values, legend
else:
return values
assign_discrete_colors(df, column, cmap, to_rgb=True, return_type='array')
¶
Assigns unique colors to each category in a categorical column of a dataframe.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
df |
pandas.DataFrame |
The input dataframe. |
required |
column |
str |
The name of the categorical column. |
required |
cmap |
dict |
A dictionary mapping categories to colors. |
required |
to_rgb |
bool |
Whether to convert the colors to RGB values. Defaults to True. |
True |
return_type |
str |
The type of the returned values. Can be 'list' or 'array'. Defaults to 'array'. |
'array' |
Returns:
Type | Description |
---|---|
list |
A list of colors for each category in the categorical column. |
Source code in leafmap/common.py
def assign_discrete_colors(df, column, cmap, to_rgb=True, return_type="array"):
"""
Assigns unique colors to each category in a categorical column of a dataframe.
Args:
df (pandas.DataFrame): The input dataframe.
column (str): The name of the categorical column.
cmap (dict): A dictionary mapping categories to colors.
to_rgb (bool): Whether to convert the colors to RGB values. Defaults to True.
return_type (str): The type of the returned values. Can be 'list' or 'array'. Defaults to 'array'.
Returns:
list: A list of colors for each category in the categorical column.
"""
import numpy as np
# Copy the categorical column from the original dataframe
category_column = df[column].copy()
# Map colors to the categorical values
category_column = category_column.map(cmap)
values = category_column.values.tolist()
if to_rgb:
values = [hex_to_rgb(check_color(color)) for color in values]
if return_type == "array":
values = np.array(values, dtype=np.uint8)
return values
basemap_xyz_tiles()
¶
Returns a dictionary containing a set of basemaps that are XYZ tile layers.
Returns:
Type | Description |
---|---|
dict |
A dictionary of XYZ tile layers. |
Source code in leafmap/common.py
def basemap_xyz_tiles():
"""Returns a dictionary containing a set of basemaps that are XYZ tile layers.
Returns:
dict: A dictionary of XYZ tile layers.
"""
from .leafmap import basemaps
layers_dict = {}
keys = dict(basemaps).keys()
for key in keys:
if isinstance(basemaps[key], ipyleaflet.WMSLayer):
pass
else:
layers_dict[key] = basemaps[key]
return layers_dict
bbox_to_gdf(bbox, crs='epsg:4326')
¶
Convert a bounding box to a GeoPandas GeoDataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
bbox |
list |
A bounding box in the format of [minx, miny, maxx, maxy]. |
required |
crs |
str |
The CRS of the bounding box. Defaults to 'epsg:4326'. |
'epsg:4326' |
Returns:
Type | Description |
---|---|
GeoDataFrame |
A GeoDataFrame with a single polygon. |
Source code in leafmap/common.py
def bbox_to_gdf(bbox, crs="epsg:4326"):
"""Convert a bounding box to a GeoPandas GeoDataFrame.
Args:
bbox (list): A bounding box in the format of [minx, miny, maxx, maxy].
crs (str, optional): The CRS of the bounding box. Defaults to 'epsg:4326'.
Returns:
GeoDataFrame: A GeoDataFrame with a single polygon.
"""
import geopandas as gpd
from shapely.geometry import Polygon
return gpd.GeoDataFrame(
geometry=[Polygon.from_bounds(*bbox)],
crs=crs,
)
bbox_to_geojson(bounds)
¶
Convert coordinates of a bounding box to a geojson.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
bounds |
list | tuple |
A list of coordinates representing [left, bottom, right, top] or m.bounds. |
required |
Returns:
Type | Description |
---|---|
dict |
A geojson feature. |
Source code in leafmap/common.py
def bbox_to_geojson(bounds):
"""Convert coordinates of a bounding box to a geojson.
Args:
bounds (list | tuple): A list of coordinates representing [left, bottom, right, top] or m.bounds.
Returns:
dict: A geojson feature.
"""
if isinstance(bounds, tuple) and len(bounds) == 2:
bounds = [bounds[0][1], bounds[0][0], bounds[1][1], bounds[1][0]]
return {
"geometry": {
"type": "Polygon",
"coordinates": [
[
[bounds[0], bounds[3]],
[bounds[0], bounds[1]],
[bounds[2], bounds[1]],
[bounds[2], bounds[3]],
[bounds[0], bounds[3]],
]
],
},
"type": "Feature",
}
bbox_to_polygon(bbox)
¶
Convert a bounding box to a shapely Polygon.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
bbox |
list |
A bounding box in the format of [minx, miny, maxx, maxy]. |
required |
Returns:
Type | Description |
---|---|
Polygon |
A shapely Polygon. |
Source code in leafmap/common.py
def bbox_to_polygon(bbox):
"""Convert a bounding box to a shapely Polygon.
Args:
bbox (list): A bounding box in the format of [minx, miny, maxx, maxy].
Returns:
Polygon: A shapely Polygon.
"""
from shapely.geometry import Polygon
return Polygon.from_bounds(*bbox)
blend_images(img1, img2, alpha=0.5, output=False, show=True, figsize=(12, 10), axis='off', **kwargs)
¶
Blends two images together using the addWeighted function from the OpenCV library.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
img1 |
numpy.ndarray |
The first input image on top represented as a NumPy array. |
required |
img2 |
numpy.ndarray |
The second input image at the bottom represented as a NumPy array. |
required |
alpha |
float |
The weighting factor for the first image in the blend. By default, this is set to 0.5. |
0.5 |
output |
str |
The path to the output image. Defaults to False. |
False |
show |
bool |
Whether to display the blended image. Defaults to True. |
True |
figsize |
tuple |
The size of the figure. Defaults to (12, 10). |
(12, 10) |
axis |
str |
The axis of the figure. Defaults to "off". |
'off' |
**kwargs |
Additional keyword arguments to pass to the cv2.addWeighted() function. |
{} |
Returns:
Type | Description |
---|---|
numpy.ndarray |
The blended image as a NumPy array. |
Source code in leafmap/common.py
def blend_images(
img1,
img2,
alpha=0.5,
output=False,
show=True,
figsize=(12, 10),
axis="off",
**kwargs,
):
"""
Blends two images together using the addWeighted function from the OpenCV library.
Args:
img1 (numpy.ndarray): The first input image on top represented as a NumPy array.
img2 (numpy.ndarray): The second input image at the bottom represented as a NumPy array.
alpha (float): The weighting factor for the first image in the blend. By default, this is set to 0.5.
output (str, optional): The path to the output image. Defaults to False.
show (bool, optional): Whether to display the blended image. Defaults to True.
figsize (tuple, optional): The size of the figure. Defaults to (12, 10).
axis (str, optional): The axis of the figure. Defaults to "off".
**kwargs: Additional keyword arguments to pass to the cv2.addWeighted() function.
Returns:
numpy.ndarray: The blended image as a NumPy array.
"""
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Resize the images to have the same dimensions
if isinstance(img1, str):
if img1.startswith("http"):
img1 = download_file(img1)
if not os.path.exists(img1):
raise ValueError(f"Input path {img1} does not exist.")
img1 = cv2.imread(img1)
if isinstance(img2, str):
if img2.startswith("http"):
img2 = download_file(img2)
if not os.path.exists(img2):
raise ValueError(f"Input path {img2} does not exist.")
img2 = cv2.imread(img2)
if img1.dtype == np.float32:
img1 = (img1 * 255).astype(np.uint8)
if img2.dtype == np.float32:
img2 = (img2 * 255).astype(np.uint8)
if img1.dtype != img2.dtype:
img2 = img2.astype(img1.dtype)
img1 = cv2.resize(img1, (img2.shape[1], img2.shape[0]))
# Blend the images using the addWeighted function
beta = 1 - alpha
blend_img = cv2.addWeighted(img1, alpha, img2, beta, 0, **kwargs)
if output:
array_to_image(blend_img, output, img2)
if show:
plt.figure(figsize=figsize)
plt.imshow(blend_img)
plt.axis(axis)
plt.show()
else:
return blend_img
bounds_to_xy_range(bounds)
¶
Convert bounds to x and y range to be used as input to bokeh map.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
bounds |
list |
A list of bounds in the form [(south, west), (north, east)] or [xmin, ymin, xmax, ymax]. |
required |
Returns:
Type | Description |
---|---|
tuple |
A tuple of (x_range, y_range). |
Source code in leafmap/common.py
def bounds_to_xy_range(bounds):
"""Convert bounds to x and y range to be used as input to bokeh map.
Args:
bounds (list): A list of bounds in the form [(south, west), (north, east)] or [xmin, ymin, xmax, ymax].
Returns:
tuple: A tuple of (x_range, y_range).
"""
if isinstance(bounds, tuple):
bounds = list(bounds)
elif not isinstance(bounds, list):
raise TypeError("bounds must be a list")
if len(bounds) == 4:
west, south, east, north = bounds
elif len(bounds) == 2:
south, west = bounds[0]
north, east = bounds[1]
xmin, ymin = lnglat_to_meters(west, south)
xmax, ymax = lnglat_to_meters(east, north)
x_range = (xmin, xmax)
y_range = (ymin, ymax)
return x_range, y_range
center_zoom_to_xy_range(center, zoom)
¶
Convert center and zoom to x and y range to be used as input to bokeh map.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
center |
tuple |
A tuple of (latitude, longitude). |
required |
zoom |
int |
The zoom level. |
required |
Returns:
Type | Description |
---|---|
tuple |
A tuple of (x_range, y_range). |
Source code in leafmap/common.py
def center_zoom_to_xy_range(center, zoom):
"""Convert center and zoom to x and y range to be used as input to bokeh map.
Args:
center (tuple): A tuple of (latitude, longitude).
zoom (int): The zoom level.
Returns:
tuple: A tuple of (x_range, y_range).
"""
if isinstance(center, tuple) or isinstance(center, list):
pass
else:
raise TypeError("center must be a tuple or list")
if not isinstance(zoom, int):
raise TypeError("zoom must be an integer")
latitude, longitude = center
x_range = (-179, 179)
y_range = (-70, 70)
x_full_length = x_range[1] - x_range[0]
y_full_length = y_range[1] - y_range[0]
x_length = x_full_length / 2 ** (zoom - 2)
y_length = y_full_length / 2 ** (zoom - 2)
south = latitude - y_length / 2
north = latitude + y_length / 2
west = longitude - x_length / 2
east = longitude + x_length / 2
xmin, ymin = lnglat_to_meters(west, south)
xmax, ymax = lnglat_to_meters(east, north)
x_range = (xmin, xmax)
y_range = (ymin, ymax)
return x_range, y_range
cesium_to_streamlit(html, width=800, height=600, responsive=True, scrolling=False, token_name=None, token_value=None, **kwargs)
¶
Renders an cesium HTML file in a Streamlit app. This method is a static Streamlit Component, meaning, no information is passed back from Leaflet on browser interaction.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
html |
str |
The HTML file to render. It can a local file path or a URL. |
required |
width |
int |
Width of the map. Defaults to 800. |
800 |
height |
int |
Height of the map. Defaults to 600. |
600 |
responsive |
bool |
Whether to make the map responsive. Defaults to True. |
True |
scrolling |
bool |
Whether to allow the map to scroll. Defaults to False. |
False |
token_name |
str |
The name of the token in the HTML file to be replaced. Defaults to None. |
None |
token_value |
str |
The value of the token to pass to the HTML file. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
streamlit.components |
components.html object. |
Source code in leafmap/common.py
def cesium_to_streamlit(
html,
width=800,
height=600,
responsive=True,
scrolling=False,
token_name=None,
token_value=None,
**kwargs,
):
"""Renders an cesium HTML file in a Streamlit app. This method is a static Streamlit Component, meaning, no information is passed back from Leaflet on browser interaction.
Args:
html (str): The HTML file to render. It can a local file path or a URL.
width (int, optional): Width of the map. Defaults to 800.
height (int, optional): Height of the map. Defaults to 600.
responsive (bool, optional): Whether to make the map responsive. Defaults to True.
scrolling (bool, optional): Whether to allow the map to scroll. Defaults to False.
token_name (str, optional): The name of the token in the HTML file to be replaced. Defaults to None.
token_value (str, optional): The value of the token to pass to the HTML file. Defaults to None.
Returns:
streamlit.components: components.html object.
"""
if token_name is None:
token_name = "your_access_token"
if token_value is None:
token_value = os.environ.get("CESIUM_TOKEN")
html_to_streamlit(
html, width, height, responsive, scrolling, token_name, token_value
)
check_cmap(cmap)
¶
Check the colormap and return a list of colors.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
cmap |
str | list | Box |
The colormap to check. |
required |
Returns:
Type | Description |
---|---|
list |
A list of colors. |
Source code in leafmap/common.py
def check_cmap(cmap):
"""Check the colormap and return a list of colors.
Args:
cmap (str | list | Box): The colormap to check.
Returns:
list: A list of colors.
"""
from box import Box
from .colormaps import get_palette
if isinstance(cmap, str):
try:
return get_palette(cmap)
except Exception as e:
raise Exception(f"{cmap} is not a valid colormap.")
elif isinstance(cmap, Box):
return list(cmap["default"])
elif isinstance(cmap, list) or isinstance(cmap, tuple):
return cmap
else:
raise Exception(f"{cmap} is not a valid colormap.")
check_color(in_color)
¶
Checks the input color and returns the corresponding hex color code.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_color |
str or tuple or list |
It can be a string (e.g., 'red', '#ffff00', 'ffff00', 'ff0') or RGB tuple (e.g., (255, 127, 0)). |
required |
Returns:
Type | Description |
---|---|
str |
A hex color code. |
Source code in leafmap/common.py
def check_color(in_color: Union[str, Tuple]) -> str:
"""Checks the input color and returns the corresponding hex color code.
Args:
in_color (str or tuple or list): It can be a string (e.g., 'red', '#ffff00', 'ffff00', 'ff0') or RGB tuple (e.g., (255, 127, 0)).
Returns:
str: A hex color code.
"""
import colour
out_color = "#000000" # default black color
if (isinstance(in_color, tuple) or isinstance(in_color, list)) and len(
in_color
) == 3:
# rescale color if necessary
if all(isinstance(item, int) for item in in_color):
in_color = [c / 255.0 for c in in_color]
return colour.Color(rgb=tuple(in_color)).hex_l
else:
# try to guess the color system
try:
return colour.Color(in_color).hex_l
except Exception as e:
pass
# try again by adding an extra # (GEE handle hex codes without #)
try:
return colour.Color(f"#{in_color}").hex_l
except Exception as e:
print(
f"The provided color ({in_color}) is invalid. Using the default black color."
)
print(e)
return out_color
check_dir(dir_path, make_dirs=True)
¶
Checks if a directory exists and creates it if it does not.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
dir_path |
[str |
The path to the directory. |
required |
make_dirs |
bool |
Whether to create the directory if it does not exist. Defaults to True. |
True |
Exceptions:
Type | Description |
---|---|
FileNotFoundError |
If the directory could not be found. |
TypeError |
If the input directory path is not a string. |
Returns:
Type | Description |
---|---|
str |
The path to the directory. |
Source code in leafmap/common.py
def check_dir(dir_path, make_dirs=True):
"""Checks if a directory exists and creates it if it does not.
Args:
dir_path ([str): The path to the directory.
make_dirs (bool, optional): Whether to create the directory if it does not exist. Defaults to True.
Raises:
FileNotFoundError: If the directory could not be found.
TypeError: If the input directory path is not a string.
Returns:
str: The path to the directory.
"""
if isinstance(dir_path, str):
if dir_path.startswith("~"):
dir_path = os.path.expanduser(dir_path)
else:
dir_path = os.path.abspath(dir_path)
if not os.path.exists(dir_path) and make_dirs:
os.makedirs(dir_path)
if os.path.exists(dir_path):
return dir_path
else:
raise FileNotFoundError("The provided directory could not be found.")
else:
raise TypeError("The provided directory path must be a string.")
check_file_path(file_path, make_dirs=True)
¶
Gets the absolute file path.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
file_path |
str |
The path to the file. |
required |
make_dirs |
bool |
Whether to create the directory if it does not exist. Defaults to True. |
True |
Exceptions:
Type | Description |
---|---|
FileNotFoundError |
If the directory could not be found. |
TypeError |
If the input directory path is not a string. |
Returns:
Type | Description |
---|---|
str |
The absolute path to the file. |
Source code in leafmap/common.py
def check_file_path(file_path, make_dirs=True):
"""Gets the absolute file path.
Args:
file_path (str): The path to the file.
make_dirs (bool, optional): Whether to create the directory if it does not exist. Defaults to True.
Raises:
FileNotFoundError: If the directory could not be found.
TypeError: If the input directory path is not a string.
Returns:
str: The absolute path to the file.
"""
if isinstance(file_path, str):
if file_path.startswith("~"):
file_path = os.path.expanduser(file_path)
else:
file_path = os.path.abspath(file_path)
file_dir = os.path.dirname(file_path)
if not os.path.exists(file_dir) and make_dirs:
os.makedirs(file_dir)
return file_path
else:
raise TypeError("The provided file path must be a string.")
check_html_string(html_string)
¶
Check if an HTML string contains local images and convert them to base64.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
html_string |
str |
The HTML string. |
required |
Returns:
Type | Description |
---|---|
str |
The HTML string with local images converted to base64. |
Source code in leafmap/common.py
def check_html_string(html_string):
"""Check if an HTML string contains local images and convert them to base64.
Args:
html_string (str): The HTML string.
Returns:
str: The HTML string with local images converted to base64.
"""
import re
import base64
# Search for img tags with src attribute
img_regex = r'<img[^>]+src\s*=\s*["\']([^"\':]+)["\'][^>]*>'
for match in re.findall(img_regex, html_string):
with open(match, "rb") as img_file:
img_data = img_file.read()
base64_data = base64.b64encode(img_data).decode("utf-8")
html_string = html_string.replace(
'src="{}"'.format(match),
'src="data:image/png;base64,' + base64_data + '"',
)
return html_string
check_url(url)
¶
Check if an HTTP URL is working.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
The URL to check. |
required |
Returns:
Type | Description |
---|---|
bool |
True if the URL is working (returns a 200 status code), False otherwise. |
Source code in leafmap/common.py
def check_url(url: str) -> bool:
"""Check if an HTTP URL is working.
Args:
url (str): The URL to check.
Returns:
bool: True if the URL is working (returns a 200 status code), False otherwise.
"""
try:
response = requests.get(url)
if response.status_code == 200:
return True
else:
return False
except requests.exceptions.RequestException:
return False
classify(data, column, cmap=None, colors=None, labels=None, scheme='Quantiles', k=5, legend_kwds=None, classification_kwds=None)
¶
Classify a dataframe column using a variety of classification schemes.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
data |
str | pd.DataFrame | gpd.GeoDataFrame |
The data to classify. It can be a filepath to a vector dataset, a pandas dataframe, or a geopandas geodataframe. |
required |
column |
str |
The column to classify. |
required |
cmap |
str |
The name of a colormap recognized by matplotlib. Defaults to None. |
None |
colors |
list |
A list of colors to use for the classification. Defaults to None. |
None |
labels |
list |
A list of labels to use for the legend. Defaults to None. |
None |
scheme |
str |
Name of a choropleth classification scheme (requires mapclassify). Name of a choropleth classification scheme (requires mapclassify). A mapclassify.MapClassifier object will be used under the hood. Supported are all schemes provided by mapclassify (e.g. 'BoxPlot', 'EqualInterval', 'FisherJenks', 'FisherJenksSampled', 'HeadTailBreaks', 'JenksCaspall', 'JenksCaspallForced', 'JenksCaspallSampled', 'MaxP', 'MaximumBreaks', 'NaturalBreaks', 'Quantiles', 'Percentiles', 'StdMean', 'UserDefined'). Arguments can be passed in classification_kwds. |
'Quantiles' |
k |
int |
Number of classes (ignored if scheme is None or if column is categorical). Default to 5. |
5 |
legend_kwds |
dict |
Keyword arguments to pass to :func: |
None |
classification_kwds |
dict |
Keyword arguments to pass to mapclassify. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
pd.DataFrame, dict |
A pandas dataframe with the classification applied and a legend dictionary. |
Source code in leafmap/common.py
def classify(
data,
column,
cmap=None,
colors=None,
labels=None,
scheme="Quantiles",
k=5,
legend_kwds=None,
classification_kwds=None,
):
"""Classify a dataframe column using a variety of classification schemes.
Args:
data (str | pd.DataFrame | gpd.GeoDataFrame): The data to classify. It can be a filepath to a vector dataset, a pandas dataframe, or a geopandas geodataframe.
column (str): The column to classify.
cmap (str, optional): The name of a colormap recognized by matplotlib. Defaults to None.
colors (list, optional): A list of colors to use for the classification. Defaults to None.
labels (list, optional): A list of labels to use for the legend. Defaults to None.
scheme (str, optional): Name of a choropleth classification scheme (requires mapclassify).
Name of a choropleth classification scheme (requires mapclassify).
A mapclassify.MapClassifier object will be used
under the hood. Supported are all schemes provided by mapclassify (e.g.
'BoxPlot', 'EqualInterval', 'FisherJenks', 'FisherJenksSampled',
'HeadTailBreaks', 'JenksCaspall', 'JenksCaspallForced',
'JenksCaspallSampled', 'MaxP', 'MaximumBreaks',
'NaturalBreaks', 'Quantiles', 'Percentiles', 'StdMean',
'UserDefined'). Arguments can be passed in classification_kwds.
k (int, optional): Number of classes (ignored if scheme is None or if column is categorical). Default to 5.
legend_kwds (dict, optional): Keyword arguments to pass to :func:`matplotlib.pyplot.legend` or `matplotlib.pyplot.colorbar`. Defaults to None.
Keyword arguments to pass to :func:`matplotlib.pyplot.legend` or
Additional accepted keywords when `scheme` is specified:
fmt : string
A formatting specification for the bin edges of the classes in the
legend. For example, to have no decimals: ``{"fmt": "{:.0f}"}``.
labels : list-like
A list of legend labels to override the auto-generated labblels.
Needs to have the same number of elements as the number of
classes (`k`).
interval : boolean (default False)
An option to control brackets from mapclassify legend.
If True, open/closed interval brackets are shown in the legend.
classification_kwds (dict, optional): Keyword arguments to pass to mapclassify. Defaults to None.
Returns:
pd.DataFrame, dict: A pandas dataframe with the classification applied and a legend dictionary.
"""
import numpy as np
import pandas as pd
import geopandas as gpd
import matplotlib as mpl
import matplotlib.pyplot as plt
try:
import mapclassify
except ImportError:
raise ImportError(
"mapclassify is required for this function. Install with `pip install mapclassify`."
)
if (
isinstance(data, gpd.GeoDataFrame)
or isinstance(data, pd.DataFrame)
or isinstance(data, pd.Series)
):
df = data
else:
try:
df = gpd.read_file(data)
except Exception:
raise TypeError(
"Data must be a GeoDataFrame or a path to a file that can be read by geopandas.read_file()."
)
if df.empty:
warnings.warn(
"The GeoDataFrame you are attempting to plot is "
"empty. Nothing has been displayed.",
UserWarning,
)
return
columns = df.columns.values.tolist()
if column not in columns:
raise ValueError(
f"{column} is not a column in the GeoDataFrame. It must be one of {columns}."
)
# Convert categorical data to numeric
init_column = None
value_list = None
if np.issubdtype(df[column].dtype, np.object_):
value_list = df[column].unique().tolist()
value_list.sort()
df["category"] = df[column].replace(value_list, range(0, len(value_list)))
init_column = column
column = "category"
k = len(value_list)
if legend_kwds is not None:
legend_kwds = legend_kwds.copy()
# To accept pd.Series and np.arrays as column
if isinstance(column, (np.ndarray, pd.Series)):
if column.shape[0] != df.shape[0]:
raise ValueError(
"The dataframe and given column have different number of rows."
)
else:
values = column
# Make sure index of a Series matches index of df
if isinstance(values, pd.Series):
values = values.reindex(df.index)
else:
values = df[column]
values = df[column]
nan_idx = np.asarray(pd.isna(values), dtype="bool")
if cmap is None:
cmap = "Blues"
try:
cmap = plt.get_cmap(cmap, k)
except:
cmap = plt.cm.get_cmap(cmap, k)
if colors is None:
colors = [mpl.colors.rgb2hex(cmap(i))[1:] for i in range(cmap.N)]
colors = ["#" + i for i in colors]
elif isinstance(colors, list):
colors = [check_color(i) for i in colors]
elif isinstance(colors, str):
colors = [check_color(colors)] * k
allowed_schemes = [
"BoxPlot",
"EqualInterval",
"FisherJenks",
"FisherJenksSampled",
"HeadTailBreaks",
"JenksCaspall",
"JenksCaspallForced",
"JenksCaspallSampled",
"MaxP",
"MaximumBreaks",
"NaturalBreaks",
"Quantiles",
"Percentiles",
"StdMean",
"UserDefined",
]
if scheme.lower() not in [s.lower() for s in allowed_schemes]:
raise ValueError(
f"{scheme} is not a valid scheme. It must be one of {allowed_schemes}."
)
if classification_kwds is None:
classification_kwds = {}
if "k" not in classification_kwds:
classification_kwds["k"] = k
binning = mapclassify.classify(
np.asarray(values[~nan_idx]), scheme, **classification_kwds
)
df["category"] = binning.yb
df["color"] = [colors[i] for i in df["category"]]
if legend_kwds is None:
legend_kwds = {}
if "interval" not in legend_kwds:
legend_kwds["interval"] = True
if "fmt" not in legend_kwds:
if np.issubdtype(df[column].dtype, np.floating):
legend_kwds["fmt"] = "{:.2f}"
else:
legend_kwds["fmt"] = "{:.0f}"
if labels is None:
# set categorical to True for creating the legend
if legend_kwds is not None and "labels" in legend_kwds:
if len(legend_kwds["labels"]) != binning.k:
raise ValueError(
"Number of labels must match number of bins, "
"received {} labels for {} bins".format(
len(legend_kwds["labels"]), binning.k
)
)
else:
labels = list(legend_kwds.pop("labels"))
else:
# fmt = "{:.2f}"
if legend_kwds is not None and "fmt" in legend_kwds:
fmt = legend_kwds.pop("fmt")
labels = binning.get_legend_classes(fmt)
if legend_kwds is not None:
show_interval = legend_kwds.pop("interval", False)
else:
show_interval = False
if not show_interval:
labels = [c[1:-1] for c in labels]
if init_column is not None:
labels = value_list
elif isinstance(labels, list):
if len(labels) != len(colors):
raise ValueError("The number of labels must match the number of colors.")
else:
raise ValueError("labels must be a list or None.")
legend_dict = dict(zip(labels, colors))
df["category"] = df["category"] + 1
return df, legend_dict
clip_image(image, mask, output, to_cog=True)
¶
Clip an image by mask.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
str |
Path to the image file in GeoTIFF format. |
required |
mask |
str | list | dict |
The mask used to extract the image. It can be a path to vector datasets (e.g., GeoJSON, Shapefile), a list of coordinates, or m.user_roi. |
required |
output |
str |
Path to the output file. |
required |
to_cog |
bool |
Flags to indicate if you want to convert the output to COG. Defaults to True. |
True |
Exceptions:
Type | Description |
---|---|
ImportError |
If the fiona or rasterio package is not installed. |
FileNotFoundError |
If the image is not found. |
ValueError |
If the mask is not a valid GeoJSON or raster file. |
FileNotFoundError |
If the mask file is not found. |
Source code in leafmap/common.py
def clip_image(image, mask, output, to_cog=True):
"""Clip an image by mask.
Args:
image (str): Path to the image file in GeoTIFF format.
mask (str | list | dict): The mask used to extract the image. It can be a path to vector datasets (e.g., GeoJSON, Shapefile), a list of coordinates, or m.user_roi.
output (str): Path to the output file.
to_cog (bool, optional): Flags to indicate if you want to convert the output to COG. Defaults to True.
Raises:
ImportError: If the fiona or rasterio package is not installed.
FileNotFoundError: If the image is not found.
ValueError: If the mask is not a valid GeoJSON or raster file.
FileNotFoundError: If the mask file is not found.
"""
try:
import json
import fiona
import rasterio
import rasterio.mask
except ImportError as e:
raise ImportError(e)
if not os.path.exists(image):
raise FileNotFoundError(f"{image} does not exist.")
if not output.endswith(".tif"):
raise ValueError("Output must be a tif file.")
output = check_file_path(output)
if isinstance(mask, str):
if mask.startswith("http"):
mask = download_file(mask, output)
if not os.path.exists(mask):
raise FileNotFoundError(f"{mask} does not exist.")
elif isinstance(mask, list) or isinstance(mask, dict):
if isinstance(mask, list):
geojson = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {"type": "Polygon", "coordinates": [mask]},
}
],
}
else:
geojson = {
"type": "FeatureCollection",
"features": [mask],
}
mask = temp_file_path(".geojson")
with open(mask, "w") as f:
json.dump(geojson, f)
with fiona.open(mask, "r") as shapefile:
shapes = [feature["geometry"] for feature in shapefile]
with rasterio.open(image) as src:
out_image, out_transform = rasterio.mask.mask(src, shapes, crop=True)
out_meta = src.meta
out_meta.update(
{
"driver": "GTiff",
"height": out_image.shape[1],
"width": out_image.shape[2],
"transform": out_transform,
}
)
with rasterio.open(output, "w", **out_meta) as dest:
dest.write(out_image)
if to_cog:
image_to_cog(output, output)
cog_validate(source, verbose=False)
¶
Validate Cloud Optimized Geotiff.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source |
str |
A dataset path or URL. Will be opened in "r" mode. |
required |
verbose |
bool |
Whether to print the output of the validation. Defaults to False. |
False |
Exceptions:
Type | Description |
---|---|
ImportError |
If the rio-cogeo package is not installed. |
FileNotFoundError |
If the provided file could not be found. |
Returns:
Type | Description |
---|---|
tuple |
A tuple containing the validation results (True is src_path is a valid COG, List of validation errors, and a list of validation warnings). |
Source code in leafmap/common.py
def cog_validate(source, verbose=False):
"""Validate Cloud Optimized Geotiff.
Args:
source (str): A dataset path or URL. Will be opened in "r" mode.
verbose (bool, optional): Whether to print the output of the validation. Defaults to False.
Raises:
ImportError: If the rio-cogeo package is not installed.
FileNotFoundError: If the provided file could not be found.
Returns:
tuple: A tuple containing the validation results (True is src_path is a valid COG, List of validation errors, and a list of validation warnings).
"""
try:
from rio_cogeo.cogeo import cog_validate, cog_info
except ImportError:
raise ImportError(
"The rio-cogeo package is not installed. Please install it with `pip install rio-cogeo` or `conda install rio-cogeo -c conda-forge`."
)
if not source.startswith("http"):
source = check_file_path(source)
if not os.path.exists(source):
raise FileNotFoundError("The provided input file could not be found.")
if verbose:
return cog_info(source)
else:
return cog_validate(source)
connect_postgis(database, host='localhost', user=None, password=None, port=5432, use_env_var=False)
¶
Connects to a PostGIS database.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
database |
str |
Name of the database |
required |
host |
str |
Hosting server for the database. Defaults to "localhost". |
'localhost' |
user |
str |
User name to access the database. Defaults to None. |
None |
password |
str |
Password to access the database. Defaults to None. |
None |
port |
int |
Port number to connect to at the server host. Defaults to 5432. |
5432 |
use_env_var |
bool |
Whether to use environment variables. It set to True, user and password are treated as an environment variables with default values user="SQL_USER" and password="SQL_PASSWORD". Defaults to False. |
False |
Exceptions:
Type | Description |
---|---|
ValueError |
If user is not specified. |
ValueError |
If password is not specified. |
Returns:
Type | Description |
---|---|
[type] |
[description] |
Source code in leafmap/common.py
def connect_postgis(
database, host="localhost", user=None, password=None, port=5432, use_env_var=False
):
"""Connects to a PostGIS database.
Args:
database (str): Name of the database
host (str, optional): Hosting server for the database. Defaults to "localhost".
user (str, optional): User name to access the database. Defaults to None.
password (str, optional): Password to access the database. Defaults to None.
port (int, optional): Port number to connect to at the server host. Defaults to 5432.
use_env_var (bool, optional): Whether to use environment variables. It set to True, user and password are treated as an environment variables with default values user="SQL_USER" and password="SQL_PASSWORD". Defaults to False.
Raises:
ValueError: If user is not specified.
ValueError: If password is not specified.
Returns:
[type]: [description]
"""
check_package(name="geopandas", URL="https://geopandas.org")
check_package(
name="sqlalchemy",
URL="https://docs.sqlalchemy.org/en/14/intro.html#installation",
)
from sqlalchemy import create_engine
if use_env_var:
if user is not None:
user = os.getenv(user)
else:
user = os.getenv("SQL_USER")
if password is not None:
password = os.getenv(password)
else:
password = os.getenv("SQL_PASSWORD")
if user is None:
raise ValueError("user is not specified.")
if password is None:
raise ValueError("password is not specified.")
connection_string = f"postgresql://{user}:{password}@{host}:{port}/{database}"
engine = create_engine(connection_string)
return engine
construct_bbox(*args, *, buffer=0.001, crs='EPSG:4326', return_gdf=False)
¶
Construct a bounding box (bbox) geometry based on either a centroid point or bbox.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
*args |
Union[float, Tuple[float, float, float, float]] |
Coordinates for the geometry. - If 2 arguments are provided, it is interpreted as a centroid (x, y) with a buffer. - If 4 arguments are provided, it is interpreted as a bbox (minx, miny, maxx, maxy). |
() |
buffer |
float |
The buffer distance around the centroid point (default is 0.01 degrees). |
0.001 |
crs |
str |
The coordinate reference system (default is "EPSG:4326"). |
'EPSG:4326' |
return_gdf |
bool |
Whether to return a GeoDataFrame (default is False). |
False |
Returns:
Type | Description |
---|---|
shapely.geometry.Polygon |
The constructed bounding box (Polygon). |
Source code in leafmap/common.py
def construct_bbox(
*args: Union[float, Tuple[float, float, float, float]],
buffer: float = 0.001,
crs: str = "EPSG:4326",
return_gdf: bool = False,
) -> Union["Polygon", "gpd.GeoDataFrame"]:
"""
Construct a bounding box (bbox) geometry based on either a centroid point or bbox.
Args:
*args: Coordinates for the geometry.
- If 2 arguments are provided, it is interpreted as a centroid (x, y) with a buffer.
- If 4 arguments are provided, it is interpreted as a bbox (minx, miny, maxx, maxy).
buffer (float): The buffer distance around the centroid point (default is 0.01 degrees).
crs (str): The coordinate reference system (default is "EPSG:4326").
return_gdf (bool): Whether to return a GeoDataFrame (default is False).
Returns:
shapely.geometry.Polygon: The constructed bounding box (Polygon).
"""
from shapely.geometry import box
if len(args) == 2:
# Case 1: Create a bounding box around the centroid point with a buffer
x, y = args
minx, miny = x - buffer, y - buffer
maxx, maxy = x + buffer, y + buffer
geometry = box(minx, miny, maxx, maxy)
elif len(args) == 4:
# Case 2: Create a bounding box directly from the given coordinates
geometry = box(args[0], args[1], args[2], args[3])
else:
raise ValueError(
"Provide either 2 arguments for centroid (x, y) or 4 arguments for bbox (minx, miny, maxx, maxy)."
)
if return_gdf:
return gpd.GeoDataFrame(geometry=[geometry], columns=["geometry"], crs=crs)
else:
return geometry
convert_coordinates(x, y, source_crs, target_crs='epsg:4326')
¶
Convert coordinates from the source EPSG code to the target EPSG code.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
x |
float |
The x-coordinate of the point. |
required |
y |
float |
The y-coordinate of the point. |
required |
source_crs |
str |
The EPSG code of the source coordinate system. |
required |
target_crs |
str |
The EPSG code of the target coordinate system. Defaults to '4326' (EPSG code for WGS84). |
'epsg:4326' |
Returns:
Type | Description |
---|---|
tuple |
A tuple containing the converted longitude and latitude. |
Source code in leafmap/common.py
def convert_coordinates(x, y, source_crs, target_crs="epsg:4326"):
"""Convert coordinates from the source EPSG code to the target EPSG code.
Args:
x (float): The x-coordinate of the point.
y (float): The y-coordinate of the point.
source_crs (str): The EPSG code of the source coordinate system.
target_crs (str, optional): The EPSG code of the target coordinate system.
Defaults to '4326' (EPSG code for WGS84).
Returns:
tuple: A tuple containing the converted longitude and latitude.
"""
import pyproj
# Create the transformer
transformer = pyproj.Transformer.from_crs(source_crs, target_crs, always_xy=True)
# Perform the transformation
lon, lat = transformer.transform(x, y) # pylint: disable=E0633
# Return the converted coordinates
return lon, lat
convert_lidar(source, destination=None, point_format_id=None, file_version=None, **kwargs)
¶
Converts a Las from one point format to another Automatically upgrades the file version if source file version is not compatible with the new point_format_id
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source |
str | laspy.lasdatas.base.LasBase |
The source data to be converted. |
required |
destination |
str |
The destination file path. Defaults to None. |
None |
point_format_id |
int |
The new point format id (the default is None, which won't change the source format id). |
None |
file_version |
str |
The new file version. None by default which means that the file_version may be upgraded for compatibility with the new point_format. The file version will not be downgraded. |
None |
Returns:
Type | Description |
---|---|
aspy.lasdatas.base.LasBase |
The converted LasData object. |
Source code in leafmap/common.py
def convert_lidar(
source, destination=None, point_format_id=None, file_version=None, **kwargs
):
"""Converts a Las from one point format to another Automatically upgrades the file version if source file version
is not compatible with the new point_format_id
Args:
source (str | laspy.lasdatas.base.LasBase): The source data to be converted.
destination (str, optional): The destination file path. Defaults to None.
point_format_id (int, optional): The new point format id (the default is None, which won't change the source format id).
file_version (str, optional): The new file version. None by default which means that the file_version may be upgraded
for compatibility with the new point_format. The file version will not be downgraded.
Returns:
aspy.lasdatas.base.LasBase: The converted LasData object.
"""
try:
import laspy
except ImportError:
print(
"The laspy package is required for this function. Use `pip install laspy[lazrs,laszip]` to install it."
)
return
if isinstance(source, str):
source = read_lidar(source)
las = laspy.convert(
source, point_format_id=point_format_id, file_version=file_version
)
if destination is None:
return las
else:
destination = check_file_path(destination)
write_lidar(las, destination, **kwargs)
return destination
convert_to_gdf(data, geometry=None, lat=None, lon=None, crs='EPSG:4326', included=None, excluded=None, obj_to_str=False, open_args=None, **kwargs)
¶
Convert data to a GeoDataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
data |
Union[pd.DataFrame, str] |
The input data, either as a DataFrame or a file path. |
required |
geometry |
Optional[str] |
The column name containing geometry data. Defaults to None. |
None |
lat |
Optional[str] |
The column name containing latitude data. Defaults to None. |
None |
lon |
Optional[str] |
The column name containing longitude data. Defaults to None. |
None |
crs |
str |
The coordinate reference system to use. Defaults to "EPSG:4326". |
'EPSG:4326' |
included |
Optional[List[str]] |
List of columns to include. Defaults to None. |
None |
excluded |
Optional[List[str]] |
List of columns to exclude. Defaults to None. |
None |
obj_to_str |
bool |
Whether to convert object dtype columns to string. Defaults to False. |
False |
open_args |
Optional[Dict[str, Any]] |
Additional arguments for file opening functions. Defaults to None. |
None |
**kwargs |
Any |
Additional keyword arguments for GeoDataFrame creation. |
{} |
Returns:
Type | Description |
---|---|
gpd.GeoDataFrame |
The converted GeoDataFrame. |
Exceptions:
Type | Description |
---|---|
ValueError |
If the file format is unsupported or required columns are not provided. |
Source code in leafmap/common.py
def convert_to_gdf(
data: Union[pd.DataFrame, str],
geometry: Optional[str] = None,
lat: Optional[str] = None,
lon: Optional[str] = None,
crs: str = "EPSG:4326",
included: Optional[List[str]] = None,
excluded: Optional[List[str]] = None,
obj_to_str: bool = False,
open_args: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> "gpd.GeoDataFrame":
"""Convert data to a GeoDataFrame.
Args:
data (Union[pd.DataFrame, str]): The input data, either as a DataFrame or a file path.
geometry (Optional[str], optional): The column name containing geometry data. Defaults to None.
lat (Optional[str], optional): The column name containing latitude data. Defaults to None.
lon (Optional[str], optional): The column name containing longitude data. Defaults to None.
crs (str, optional): The coordinate reference system to use. Defaults to "EPSG:4326".
included (Optional[List[str]], optional): List of columns to include. Defaults to None.
excluded (Optional[List[str]], optional): List of columns to exclude. Defaults to None.
obj_to_str (bool, optional): Whether to convert object dtype columns to string. Defaults to False.
open_args (Optional[Dict[str, Any]], optional): Additional arguments for file opening functions. Defaults to None.
**kwargs (Any): Additional keyword arguments for GeoDataFrame creation.
Returns:
gpd.GeoDataFrame: The converted GeoDataFrame.
Raises:
ValueError: If the file format is unsupported or required columns are not provided.
"""
import geopandas as gpd
from shapely.geometry import Point, shape
if open_args is None:
open_args = {}
if not isinstance(data, pd.DataFrame):
if isinstance(data, str):
if data.endswith(".parquet"):
data = pd.read_parquet(data, **open_args)
elif data.endswith(".csv"):
data = pd.read_csv(data, **open_args)
elif data.endswith(".json"):
data = pd.read_json(data, **open_args)
elif data.endswith(".xlsx"):
data = pd.read_excel(data, **open_args)
else:
raise ValueError(
"Unsupported file format. Only Parquet, CSV, JSON, and Excel files are supported."
)
# If include_cols is specified, filter the DataFrame to include only those columns
if included:
if geometry:
included.append(geometry)
elif lat and lon:
included.append(lat)
included.append(lon)
data = data[included]
# Exclude specified columns if provided
if excluded:
data = data.drop(columns=excluded)
# Convert 'object' dtype columns to 'string' if obj_to_str is True
if obj_to_str:
data = data.astype(
{col: "string" for col in data.select_dtypes(include="object").columns}
)
# Handle the creation of geometry
if geometry:
def convert_geometry(x):
if isinstance(x, str):
try:
# Parse the string as JSON and then convert to a geometry
return shape(json.loads(x))
except (json.JSONDecodeError, TypeError) as e:
print(f"Error converting geometry: {e}")
return None
return x
data = data[data[geometry].notnull()]
data[geometry] = data[geometry].apply(convert_geometry)
elif lat and lon:
# Create a geometry column from latitude and longitude
data["geometry"] = data.apply(lambda row: Point(row[lon], row[lat]), axis=1)
geometry = "geometry"
else:
raise ValueError(
"Either geometry_col or both lat_col and lon_col must be provided."
)
# Convert the DataFrame to a GeoDataFrame
gdf = gpd.GeoDataFrame(data, geometry=geometry, **kwargs)
# Set CRS (assuming WGS84 by default, modify as needed)
gdf.set_crs(crs, inplace=True)
return gdf
coords_to_geojson(coords)
¶
Convert a list of bbox coordinates representing [left, bottom, right, top] to geojson FeatureCollection.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
coords |
list |
A list of bbox coordinates representing [left, bottom, right, top]. |
required |
Returns:
Type | Description |
---|---|
dict |
A geojson FeatureCollection. |
Source code in leafmap/common.py
def coords_to_geojson(coords):
"""Convert a list of bbox coordinates representing [left, bottom, right, top] to geojson FeatureCollection.
Args:
coords (list): A list of bbox coordinates representing [left, bottom, right, top].
Returns:
dict: A geojson FeatureCollection.
"""
features = []
for bbox in coords:
features.append(bbox_to_geojson(bbox))
return {"type": "FeatureCollection", "features": features}
coords_to_vector(coords, output=None, crs='EPSG:4326', **kwargs)
¶
Convert a list of coordinates to a GeoDataFrame or a vector file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
coords |
list |
A list of coordinates in the format of [(x1, y1), (x2, y2), ...]. |
required |
output |
str |
The path to the output vector file. Defaults to None. |
None |
crs |
str |
The CRS of the coordinates. Defaults to "EPSG:4326". |
'EPSG:4326' |
Returns:
Type | Description |
---|---|
gpd.GeoDataFraem |
A GeoDataFrame of the coordinates. |
Source code in leafmap/common.py
def coords_to_vector(coords, output=None, crs="EPSG:4326", **kwargs):
"""Convert a list of coordinates to a GeoDataFrame or a vector file.
Args:
coords (list): A list of coordinates in the format of [(x1, y1), (x2, y2), ...].
output (str, optional): The path to the output vector file. Defaults to None.
crs (str, optional): The CRS of the coordinates. Defaults to "EPSG:4326".
Returns:
gpd.GeoDataFraem: A GeoDataFrame of the coordinates.
"""
import geopandas as gpd
from shapely.geometry import Point
if not isinstance(coords, (list, tuple)):
raise TypeError("coords must be a list of coordinates")
if isinstance(coords[0], int) or isinstance(coords[0], float):
coords = [(coords[0], coords[1])]
# convert the points to a GeoDataFrame
geometry = [Point(xy) for xy in coords]
gdf = gpd.GeoDataFrame(geometry=geometry, crs="EPSG:4326")
gdf.to_crs(crs, inplace=True)
if output is not None:
gdf.to_file(output, **kwargs)
else:
return gdf
coords_to_xy(src_fp, coords, coord_crs='epsg:4326', request_payer='bucket-owner', env_args={}, open_args={}, **kwargs)
¶
Converts a list of coordinates to pixel coordinates, i.e., (col, row) coordinates.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
src_fp |
str |
The source raster file path. |
required |
coords |
list |
A list of coordinates in the format of [[x1, y1], [x2, y2], ...] |
required |
coord_crs |
str |
The coordinate CRS of the input coordinates. Defaults to "epsg:4326". |
'epsg:4326' |
request_payer |
Specifies who pays for the download from S3. Can be "bucket-owner" or "requester". Defaults to "bucket-owner". |
'bucket-owner' |
|
env_args |
Additional keyword arguments to pass to rasterio.Env. |
{} |
|
open_args |
Additional keyword arguments to pass to rasterio.open. |
{} |
|
**kwargs |
Additional keyword arguments to pass to rasterio.transform.rowcol. |
{} |
Returns:
Type | Description |
---|---|
list |
A list of pixel coordinates in the format of [[x1, y1], [x2, y2], ...] |
Source code in leafmap/common.py
def coords_to_xy(
src_fp: str,
coords: list,
coord_crs: str = "epsg:4326",
request_payer="bucket-owner",
env_args={},
open_args={},
**kwargs,
) -> list:
"""Converts a list of coordinates to pixel coordinates, i.e., (col, row) coordinates.
Args:
src_fp: The source raster file path.
coords: A list of coordinates in the format of [[x1, y1], [x2, y2], ...]
coord_crs: The coordinate CRS of the input coordinates. Defaults to "epsg:4326".
request_payer: Specifies who pays for the download from S3.
Can be "bucket-owner" or "requester". Defaults to "bucket-owner".
env_args: Additional keyword arguments to pass to rasterio.Env.
open_args: Additional keyword arguments to pass to rasterio.open.
**kwargs: Additional keyword arguments to pass to rasterio.transform.rowcol.
Returns:
A list of pixel coordinates in the format of [[x1, y1], [x2, y2], ...]
"""
import numpy as np
import rasterio
if isinstance(coords, np.ndarray):
coords = coords.tolist()
if len(coords) == 4 and all([isinstance(c, (int, float)) for c in coords]):
coords = [[coords[0], coords[1]], [coords[2], coords[3]]]
xs, ys = zip(*coords)
with rasterio.Env(AWS_REQUEST_PAYER=request_payer, **env_args):
with rasterio.open(src_fp, **open_args) as src:
width = src.width
height = src.height
if coord_crs != src.crs:
xs, ys = transform_coords(
xs, ys, coord_crs, src.crs, **kwargs
) # pylint: disable=E0633
rows, cols = rasterio.transform.rowcol(src.transform, xs, ys, **kwargs)
result = [[col, row] for col, row in zip(cols, rows)]
result = [
[x, y] for x, y in result if x >= 0 and y >= 0 and x < width and y < height
]
if len(result) == 0:
print("No valid pixel coordinates found.")
elif len(result) < len(coords):
print("Some coordinates are out of the image boundary.")
return result
create_code_cell(code='', where='below')
¶
Creates a code cell in the IPython Notebook.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
code |
str |
Code to fill the new code cell with. Defaults to ''. |
'' |
where |
str |
Where to add the new code cell. It can be one of the following: above, below, at_bottom. Defaults to 'below'. |
'below' |
Source code in leafmap/common.py
def create_code_cell(code="", where="below"):
"""Creates a code cell in the IPython Notebook.
Args:
code (str, optional): Code to fill the new code cell with. Defaults to ''.
where (str, optional): Where to add the new code cell. It can be one of the following: above, below, at_bottom. Defaults to 'below'.
"""
import base64
# try:
# import pyperclip
# except ImportError:
# install_package("pyperclip")
# import pyperclip
from IPython.display import Javascript, display
# try:
# pyperclip.copy(str(code))
# except Exception as e:
# pass
encoded_code = (base64.b64encode(str.encode(code))).decode()
display(
Javascript(
"""
var code = IPython.notebook.insert_cell_{0}('code');
code.set_text(atob("{1}"));
""".format(
where, encoded_code
)
)
)
create_download_link(filename, title='Click here to download: ')
¶
Downloads a file from voila. Adopted from https://github.com/voila-dashboards/voila/issues/578
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filename |
str |
The file path to the file to download |
required |
title |
str |
str. Defaults to "Click here to download: ". |
'Click here to download: ' |
Returns:
Type | Description |
---|---|
str |
HTML download URL. |
Source code in leafmap/common.py
def create_download_link(filename, title="Click here to download: "):
"""Downloads a file from voila. Adopted from https://github.com/voila-dashboards/voila/issues/578
Args:
filename (str): The file path to the file to download
title (str, optional): str. Defaults to "Click here to download: ".
Returns:
str: HTML download URL.
"""
import base64
from IPython.display import HTML
data = open(filename, "rb").read()
b64 = base64.b64encode(data)
payload = b64.decode()
basename = os.path.basename(filename)
html = '<a download="{filename}" href="data:text/csv;base64,{payload}" style="color:#0000FF;" target="_blank">{title}</a>'
html = html.format(payload=payload, title=title + f" {basename}", filename=basename)
return HTML(html)
create_legend(title='Legend', labels=None, colors=None, legend_dict=None, builtin_legend=None, opacity=1.0, position='bottomright', draggable=True, output=None, style={})
¶
Create a legend in HTML format. Reference: https://bit.ly/3oV6vnH
Parameters:
Name | Type | Description | Default |
---|---|---|---|
title |
str |
Title of the legend. Defaults to 'Legend'. Defaults to "Legend". |
'Legend' |
colors |
list |
A list of legend colors. Defaults to None. |
None |
labels |
list |
A list of legend labels. Defaults to None. |
None |
legend_dict |
dict |
A dictionary containing legend items as keys and color as values. If provided, legend_keys and legend_colors will be ignored. Defaults to None. |
None |
builtin_legend |
str |
Name of the builtin legend to add to the map. Defaults to None. |
None |
opacity |
float |
The opacity of the legend. Defaults to 1.0. |
1.0 |
position |
str |
The position of the legend, can be one of the following: "topleft", "topright", "bottomleft", "bottomright". Defaults to "bottomright". |
'bottomright' |
draggable |
bool |
If True, the legend can be dragged to a new position. Defaults to True. |
True |
output |
str |
The output file path (*.html) to save the legend. Defaults to None. |
None |
style |
Additional keyword arguments to style the legend, such as position, bottom, right, z-index, border, background-color, border-radius, padding, font-size, etc. The default style is: style = { 'position': 'fixed', 'z-index': '9999', 'border': '2px solid grey', 'background-color': 'rgba(255, 255, 255, 0.8)', 'border-radius': '5px', 'padding': '10px', 'font-size': '14px', 'bottom': '20px', 'right': '5px' } |
{} |
Returns:
Type | Description |
---|---|
str |
The HTML code of the legend. |
Source code in leafmap/common.py
def create_legend(
title="Legend",
labels=None,
colors=None,
legend_dict=None,
builtin_legend=None,
opacity=1.0,
position="bottomright",
draggable=True,
output=None,
style={},
):
"""Create a legend in HTML format. Reference: https://bit.ly/3oV6vnH
Args:
title (str, optional): Title of the legend. Defaults to 'Legend'. Defaults to "Legend".
colors (list, optional): A list of legend colors. Defaults to None.
labels (list, optional): A list of legend labels. Defaults to None.
legend_dict (dict, optional): A dictionary containing legend items as keys and color as values.
If provided, legend_keys and legend_colors will be ignored. Defaults to None.
builtin_legend (str, optional): Name of the builtin legend to add to the map. Defaults to None.
opacity (float, optional): The opacity of the legend. Defaults to 1.0.
position (str, optional): The position of the legend, can be one of the following:
"topleft", "topright", "bottomleft", "bottomright". Defaults to "bottomright".
draggable (bool, optional): If True, the legend can be dragged to a new position. Defaults to True.
output (str, optional): The output file path (*.html) to save the legend. Defaults to None.
style: Additional keyword arguments to style the legend, such as position, bottom, right, z-index,
border, background-color, border-radius, padding, font-size, etc. The default style is:
style = {
'position': 'fixed',
'z-index': '9999',
'border': '2px solid grey',
'background-color': 'rgba(255, 255, 255, 0.8)',
'border-radius': '5px',
'padding': '10px',
'font-size': '14px',
'bottom': '20px',
'right': '5px'
}
Returns:
str: The HTML code of the legend.
"""
import pkg_resources
from .legends import builtin_legends
pkg_dir = os.path.dirname(pkg_resources.resource_filename("leafmap", "leafmap.py"))
legend_template = os.path.join(pkg_dir, "data/template/legend_style.html")
if draggable:
legend_template = os.path.join(pkg_dir, "data/template/legend.txt")
if not os.path.exists(legend_template):
raise FileNotFoundError("The legend template does not exist.")
if labels is not None:
if not isinstance(labels, list):
print("The legend keys must be a list.")
return
else:
labels = ["One", "Two", "Three", "Four", "etc"]
if colors is not None:
if not isinstance(colors, list):
print("The legend colors must be a list.")
return
elif all(isinstance(item, tuple) for item in colors):
try:
colors = [rgb_to_hex(x) for x in colors]
except Exception as e:
print(e)
elif all((item.startswith("#") and len(item) == 7) for item in colors):
pass
elif all((len(item) == 6) for item in colors):
pass
else:
print("The legend colors must be a list of tuples.")
return
else:
colors = [
"#8DD3C7",
"#FFFFB3",
"#BEBADA",
"#FB8072",
"#80B1D3",
]
if len(labels) != len(colors):
print("The legend keys and values must be the same length.")
return
allowed_builtin_legends = builtin_legends.keys()
if builtin_legend is not None:
if builtin_legend not in allowed_builtin_legends:
print(
"The builtin legend must be one of the following: {}".format(
", ".join(allowed_builtin_legends)
)
)
return
else:
legend_dict = builtin_legends[builtin_legend]
labels = list(legend_dict.keys())
colors = list(legend_dict.values())
if legend_dict is not None:
if not isinstance(legend_dict, dict):
print("The legend dict must be a dictionary.")
return
else:
labels = list(legend_dict.keys())
colors = list(legend_dict.values())
if all(isinstance(item, tuple) for item in colors):
try:
colors = [rgb_to_hex(x) for x in colors]
except Exception as e:
print(e)
allowed_positions = [
"topleft",
"topright",
"bottomleft",
"bottomright",
]
if position not in allowed_positions:
raise ValueError(
"The position must be one of the following: {}".format(
", ".join(allowed_positions)
)
)
if position == "bottomright":
if "bottom" not in style:
style["bottom"] = "20px"
if "right" not in style:
style["right"] = "5px"
if "left" in style:
del style["left"]
if "top" in style:
del style["top"]
elif position == "bottomleft":
if "bottom" not in style:
style["bottom"] = "5px"
if "left" not in style:
style["left"] = "5px"
if "right" in style:
del style["right"]
if "top" in style:
del style["top"]
elif position == "topright":
if "top" not in style:
style["top"] = "5px"
if "right" not in style:
style["right"] = "5px"
if "left" in style:
del style["left"]
if "bottom" in style:
del style["bottom"]
elif position == "topleft":
if "top" not in style:
style["top"] = "5px"
if "left" not in style:
style["left"] = "5px"
if "right" in style:
del style["right"]
if "bottom" in style:
del style["bottom"]
if "position" not in style:
style["position"] = "fixed"
if "z-index" not in style:
style["z-index"] = "9999"
if "background-color" not in style:
style["background-color"] = "rgba(255, 255, 255, 0.8)"
if "padding" not in style:
style["padding"] = "10px"
if "border-radius" not in style:
style["border-radius"] = "5px"
if "font-size" not in style:
style["font-size"] = "14px"
content = []
with open(legend_template) as f:
lines = f.readlines()
if draggable:
for index, line in enumerate(lines):
if index < 36:
content.append(line)
elif index == 36:
line = lines[index].replace("Legend", title)
content.append(line)
elif index < 39:
content.append(line)
elif index == 39:
for i, color in enumerate(colors):
item = f" <li><span style='background:{check_color(color)};opacity:{opacity};'></span>{labels[i]}</li>\n"
content.append(item)
elif index > 41:
content.append(line)
content = content[3:-1]
else:
for index, line in enumerate(lines):
if index < 8:
content.append(line)
elif index == 8:
for key, value in style.items():
content.append(
" {}: {};\n".format(key.replace("_", "-"), value)
)
elif index < 17:
pass
elif index < 19:
content.append(line)
elif index == 19:
content.append(line.replace("Legend", title))
elif index < 22:
content.append(line)
elif index == 22:
for index, key in enumerate(labels):
color = colors[index]
if not color.startswith("#"):
color = "#" + color
item = " <li><span style='background:{};opacity:{};'></span>{}</li>\n".format(
color, opacity, key
)
content.append(item)
elif index < 33:
pass
else:
content.append(line)
legend_text = "".join(content)
if output is not None:
with open(output, "w") as f:
f.write(legend_text)
else:
return legend_text
create_timelapse(images, out_gif, ext='.tif', bands=None, size=None, bbox=None, fps=5, loop=0, add_progress_bar=True, progress_bar_color='blue', progress_bar_height=5, add_text=False, text_xy=None, text_sequence=None, font_type='arial.ttf', font_size=20, font_color='black', mp4=False, quiet=True, reduce_size=False, clean_up=True, **kwargs)
¶
Creates a timelapse gif from a list of images.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
images |
list | str |
The list of images or input directory to create the gif from. For example, '/path/to/images/*.tif' or ['/path/to/image1.tif', '/path/to/image2.tif', ...] |
required |
out_gif |
str |
File path to the output gif. |
required |
ext |
str |
The extension of the images. Defaults to '.tif'. |
'.tif' |
bands |
list |
The bands to use for the gif. For example, [0, 1, 2] for RGB, and [0] for grayscale. Defaults to None. |
None |
size |
tuple |
The size of the gif. For example, (500, 500). Defaults to None, using the original size. |
None |
bbox |
list |
The bounding box of the gif. For example, [xmin, ymin, xmax, ymax]. Defaults to None, using the original bounding box. |
None |
fps |
int |
The frames per second of the gif. Defaults to 5. |
5 |
loop |
int |
The number of times to loop the gif. Defaults to 0, looping forever. |
0 |
add_progress_bar |
bool |
Whether to add a progress bar to the gif. Defaults to True. |
True |
progress_bar_color |
str |
The color of the progress bar, can be color name or hex code. Defaults to 'blue'. |
'blue' |
progress_bar_height |
int |
The height of the progress bar. Defaults to 5. |
5 |
add_text |
bool |
Whether to add text to the gif. Defaults to False. |
False |
text_xy |
tuple |
The x, y coordinates of the text. For example, ('10%', '10%'). Defaults to None, using the bottom left corner. |
None |
text_sequence |
list |
The sequence of text to add to the gif. For example, ['year 1', 'year 2', ...]. |
None |
font_type |
str |
The font type of the text, can be 'arial.ttf' or 'alibaba.otf', or any system font. Defaults to 'arial.ttf'. |
'arial.ttf' |
font_size |
int |
The font size of the text. Defaults to 20. |
20 |
font_color |
str |
The color of the text, can be color name or hex code. Defaults to 'black'. |
'black' |
mp4 |
bool |
Whether to convert the gif to mp4. Defaults to False. |
False |
quiet |
bool |
Whether to print the progress. Defaults to False. |
True |
reduce_size |
bool |
Whether to reduce the size of the gif using ffmpeg. Defaults to False. |
False |
clean_up |
bool |
Whether to clean up the temporary files. Defaults to True. |
True |
Source code in leafmap/common.py
def create_timelapse(
images: Union[List, str],
out_gif: str,
ext: str = ".tif",
bands: Optional[List] = None,
size: Optional[Tuple] = None,
bbox: Optional[List] = None,
fps: int = 5,
loop: int = 0,
add_progress_bar: bool = True,
progress_bar_color: str = "blue",
progress_bar_height: int = 5,
add_text: bool = False,
text_xy: Optional[Tuple] = None,
text_sequence: Optional[List] = None,
font_type: str = "arial.ttf",
font_size: int = 20,
font_color: str = "black",
mp4: bool = False,
quiet: bool = True,
reduce_size: bool = False,
clean_up: bool = True,
**kwargs,
):
"""Creates a timelapse gif from a list of images.
Args:
images (list | str): The list of images or input directory to create the gif from.
For example, '/path/to/images/*.tif' or ['/path/to/image1.tif', '/path/to/image2.tif', ...]
out_gif (str): File path to the output gif.
ext (str, optional): The extension of the images. Defaults to '.tif'.
bands (list, optional): The bands to use for the gif. For example, [0, 1, 2] for RGB, and [0] for grayscale. Defaults to None.
size (tuple, optional): The size of the gif. For example, (500, 500). Defaults to None, using the original size.
bbox (list, optional): The bounding box of the gif. For example, [xmin, ymin, xmax, ymax]. Defaults to None, using the original bounding box.
fps (int, optional): The frames per second of the gif. Defaults to 5.
loop (int, optional): The number of times to loop the gif. Defaults to 0, looping forever.
add_progress_bar (bool, optional): Whether to add a progress bar to the gif. Defaults to True.
progress_bar_color (str, optional): The color of the progress bar, can be color name or hex code. Defaults to 'blue'.
progress_bar_height (int, optional): The height of the progress bar. Defaults to 5.
add_text (bool, optional): Whether to add text to the gif. Defaults to False.
text_xy (tuple, optional): The x, y coordinates of the text. For example, ('10%', '10%').
Defaults to None, using the bottom left corner.
text_sequence (list, optional): The sequence of text to add to the gif. For example, ['year 1', 'year 2', ...].
font_type (str, optional): The font type of the text, can be 'arial.ttf' or 'alibaba.otf', or any system font. Defaults to 'arial.ttf'.
font_size (int, optional): The font size of the text. Defaults to 20.
font_color (str, optional): The color of the text, can be color name or hex code. Defaults to 'black'.
mp4 (bool, optional): Whether to convert the gif to mp4. Defaults to False.
quiet (bool, optional): Whether to print the progress. Defaults to False.
reduce_size (bool, optional): Whether to reduce the size of the gif using ffmpeg. Defaults to False.
clean_up (bool, optional): Whether to clean up the temporary files. Defaults to True.
"""
import glob
import tempfile
if isinstance(images, str):
if not images.endswith(ext):
images = os.path.join(images, f"*{ext}")
images = list(glob.glob(images))
if not isinstance(images, list):
raise ValueError("images must be a list or a path to the image directory.")
images.sort()
temp_dir = os.path.join(tempfile.gettempdir(), "timelapse")
if not os.path.exists(temp_dir):
os.makedirs(temp_dir)
if bbox is not None:
clip_dir = os.path.join(tempfile.gettempdir(), "clip")
if not os.path.exists(clip_dir):
os.makedirs(clip_dir)
if len(bbox) == 4:
bbox = bbox_to_geojson(bbox)
else:
clip_dir = None
output = widgets.Output()
if "out_ext" in kwargs:
out_ext = kwargs["out_ext"].lower()
else:
out_ext = ".jpg"
try:
for index, image in enumerate(images):
if bbox is not None:
clip_file = os.path.join(clip_dir, os.path.basename(image))
with output:
clip_image(image, mask=bbox, output=clip_file, to_cog=False)
image = clip_file
if "add_prefix" in kwargs:
basename = (
str(f"{index + 1}").zfill(len(str(len(images))))
+ "-"
+ os.path.basename(image).replace(ext, out_ext)
)
else:
basename = os.path.basename(image).replace(ext, out_ext)
if not quiet:
print(f"Processing {index+1}/{len(images)}: {basename} ...")
# ignore GDAL warnings
with output:
numpy_to_image(
image, os.path.join(temp_dir, basename), bands=bands, size=size
)
make_gif(
temp_dir,
out_gif,
ext=out_ext,
fps=fps,
loop=loop,
mp4=mp4,
clean_up=clean_up,
)
if clip_dir is not None:
shutil.rmtree(clip_dir)
if add_text:
add_text_to_gif(
out_gif,
out_gif,
text_xy,
text_sequence,
font_type,
font_size,
font_color,
add_progress_bar,
progress_bar_color,
progress_bar_height,
1000 / fps,
loop,
)
elif add_progress_bar:
add_progress_bar_to_gif(
out_gif,
out_gif,
progress_bar_color,
progress_bar_height,
1000 / fps,
loop,
)
if reduce_size:
reduce_gif_size(out_gif)
except Exception as e:
print(e)
csv_points_to_shp(in_csv, out_shp, latitude='latitude', longitude='longitude')
¶
Converts a csv file containing points (latitude, longitude) into a shapefile.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_csv |
str |
File path or HTTP URL to the input csv file. For example, https://raw.githubusercontent.com/opengeos/data/main/world/world_cities.csv |
required |
out_shp |
str |
File path to the output shapefile. |
required |
latitude |
str |
Column name for the latitude column. Defaults to 'latitude'. |
'latitude' |
longitude |
str |
Column name for the longitude column. Defaults to 'longitude'. |
'longitude' |
Source code in leafmap/common.py
def csv_points_to_shp(in_csv, out_shp, latitude="latitude", longitude="longitude"):
"""Converts a csv file containing points (latitude, longitude) into a shapefile.
Args:
in_csv (str): File path or HTTP URL to the input csv file. For example, https://raw.githubusercontent.com/opengeos/data/main/world/world_cities.csv
out_shp (str): File path to the output shapefile.
latitude (str, optional): Column name for the latitude column. Defaults to 'latitude'.
longitude (str, optional): Column name for the longitude column. Defaults to 'longitude'.
"""
if in_csv.startswith("http") and in_csv.endswith(".csv"):
out_dir = os.path.join(os.path.expanduser("~"), "Downloads")
out_name = os.path.basename(in_csv)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
download_from_url(in_csv, out_dir=out_dir)
in_csv = os.path.join(out_dir, out_name)
wbt = whitebox.WhiteboxTools()
in_csv = os.path.abspath(in_csv)
out_shp = os.path.abspath(out_shp)
if not os.path.exists(in_csv):
raise Exception("The provided csv file does not exist.")
with open(in_csv, encoding="utf-8") as csv_file:
reader = csv.DictReader(csv_file)
fields = reader.fieldnames
xfield = fields.index(longitude)
yfield = fields.index(latitude)
wbt.csv_points_to_vector(in_csv, out_shp, xfield=xfield, yfield=yfield, epsg=4326)
csv_to_df(in_csv, **kwargs)
¶
Converts a CSV file to pandas dataframe.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_csv |
str |
File path to the input CSV. |
required |
Returns:
Type | Description |
---|---|
pd.DataFrame |
pandas DataFrame |
Source code in leafmap/common.py
def csv_to_df(in_csv, **kwargs):
"""Converts a CSV file to pandas dataframe.
Args:
in_csv (str): File path to the input CSV.
Returns:
pd.DataFrame: pandas DataFrame
"""
import pandas as pd
try:
return pd.read_csv(in_csv, **kwargs)
except Exception as e:
raise Exception(e)
csv_to_gdf(in_csv, latitude='latitude', longitude='longitude', geometry=None, crs='EPSG:4326', encoding='utf-8', **kwargs)
¶
Creates points for a CSV file and converts them to a GeoDataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_csv |
str |
The file path to the input CSV file. |
required |
latitude |
str |
The name of the column containing latitude coordinates. Defaults to "latitude". |
'latitude' |
longitude |
str |
The name of the column containing longitude coordinates. Defaults to "longitude". |
'longitude' |
geometry |
str |
The name of the column containing geometry. Defaults to None. |
None |
crs |
str |
The coordinate reference system. Defaults to "EPSG:4326". |
'EPSG:4326' |
encoding |
str |
The encoding of characters. Defaults to "utf-8". |
'utf-8' |
Returns:
Type | Description |
---|---|
object |
GeoDataFrame. |
Source code in leafmap/common.py
def csv_to_gdf(
in_csv,
latitude="latitude",
longitude="longitude",
geometry=None,
crs="EPSG:4326",
encoding="utf-8",
**kwargs,
):
"""Creates points for a CSV file and converts them to a GeoDataFrame.
Args:
in_csv (str): The file path to the input CSV file.
latitude (str, optional): The name of the column containing latitude coordinates. Defaults to "latitude".
longitude (str, optional): The name of the column containing longitude coordinates. Defaults to "longitude".
geometry (str, optional): The name of the column containing geometry. Defaults to None.
crs (str, optional): The coordinate reference system. Defaults to "EPSG:4326".
encoding (str, optional): The encoding of characters. Defaults to "utf-8".
Returns:
object: GeoDataFrame.
"""
check_package(name="geopandas", URL="https://geopandas.org")
import geopandas as gpd
import pandas as pd
from shapely import wkt
out_dir = os.getcwd()
if geometry is None:
out_geojson = os.path.join(out_dir, random_string() + ".geojson")
csv_to_geojson(in_csv, out_geojson, latitude, longitude, encoding=encoding)
gdf = gpd.read_file(out_geojson)
os.remove(out_geojson)
else:
df = pd.read_csv(in_csv, encoding=encoding)
df["geometry"] = df[geometry].apply(wkt.loads)
gdf = gpd.GeoDataFrame(df, geometry="geometry", crs=crs, **kwargs)
return gdf
csv_to_geojson(in_csv, out_geojson=None, latitude='latitude', longitude='longitude', encoding='utf-8')
¶
Creates points for a CSV file and exports data as a GeoJSON.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_csv |
str |
The file path to the input CSV file. |
required |
out_geojson |
str |
The file path to the exported GeoJSON. Default to None. |
None |
latitude |
str |
The name of the column containing latitude coordinates. Defaults to "latitude". |
'latitude' |
longitude |
str |
The name of the column containing longitude coordinates. Defaults to "longitude". |
'longitude' |
encoding |
str |
The encoding of characters. Defaults to "utf-8". |
'utf-8' |
Source code in leafmap/common.py
def csv_to_geojson(
in_csv,
out_geojson=None,
latitude="latitude",
longitude="longitude",
encoding="utf-8",
):
"""Creates points for a CSV file and exports data as a GeoJSON.
Args:
in_csv (str): The file path to the input CSV file.
out_geojson (str): The file path to the exported GeoJSON. Default to None.
latitude (str, optional): The name of the column containing latitude coordinates. Defaults to "latitude".
longitude (str, optional): The name of the column containing longitude coordinates. Defaults to "longitude".
encoding (str, optional): The encoding of characters. Defaults to "utf-8".
"""
import pandas as pd
in_csv = github_raw_url(in_csv)
if out_geojson is not None:
out_geojson = check_file_path(out_geojson)
df = pd.read_csv(in_csv)
geojson = df_to_geojson(
df, latitude=latitude, longitude=longitude, encoding=encoding
)
if out_geojson is None:
return geojson
else:
with open(out_geojson, "w", encoding=encoding) as f:
f.write(json.dumps(geojson))
csv_to_shp(in_csv, out_shp, latitude='latitude', longitude='longitude', encoding='utf-8')
¶
Converts a csv file with latlon info to a point shapefile.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_csv |
str |
The input csv file containing longitude and latitude columns. |
required |
out_shp |
str |
The file path to the output shapefile. |
required |
latitude |
str |
The column name of the latitude column. Defaults to 'latitude'. |
'latitude' |
longitude |
str |
The column name of the longitude column. Defaults to 'longitude'. |
'longitude' |
Source code in leafmap/common.py
def csv_to_shp(
in_csv, out_shp, latitude="latitude", longitude="longitude", encoding="utf-8"
):
"""Converts a csv file with latlon info to a point shapefile.
Args:
in_csv (str): The input csv file containing longitude and latitude columns.
out_shp (str): The file path to the output shapefile.
latitude (str, optional): The column name of the latitude column. Defaults to 'latitude'.
longitude (str, optional): The column name of the longitude column. Defaults to 'longitude'.
"""
import shapefile as shp
if in_csv.startswith("http") and in_csv.endswith(".csv"):
in_csv = github_raw_url(in_csv)
in_csv = download_file(in_csv, quiet=True, overwrite=True)
try:
points = shp.Writer(out_shp, shapeType=shp.POINT)
with open(in_csv, encoding=encoding) as csvfile:
csvreader = csv.DictReader(csvfile)
header = csvreader.fieldnames
[points.field(field) for field in header]
for row in csvreader:
points.point((float(row[longitude])), (float(row[latitude])))
points.record(*tuple([row[f] for f in header]))
out_prj = out_shp.replace(".shp", ".prj")
with open(out_prj, "w") as f:
prj_str = 'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.0174532925199433]] '
f.write(prj_str)
except Exception as e:
raise Exception(e)
csv_to_vector(in_csv, output, latitude='latitude', longitude='longitude', geometry=None, crs='EPSG:4326', encoding='utf-8', **kwargs)
¶
Creates points for a CSV file and converts them to a vector dataset.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_csv |
str |
The file path to the input CSV file. |
required |
output |
str |
The file path to the output vector dataset. |
required |
latitude |
str |
The name of the column containing latitude coordinates. Defaults to "latitude". |
'latitude' |
longitude |
str |
The name of the column containing longitude coordinates. Defaults to "longitude". |
'longitude' |
geometry |
str |
The name of the column containing geometry. Defaults to None. |
None |
crs |
str |
The coordinate reference system. Defaults to "EPSG:4326". |
'EPSG:4326' |
encoding |
str |
The encoding of characters. Defaults to "utf-8". |
'utf-8' |
**kwargs |
Additional keyword arguments to pass to gdf.to_file(). |
{} |
Source code in leafmap/common.py
def csv_to_vector(
in_csv,
output,
latitude="latitude",
longitude="longitude",
geometry=None,
crs="EPSG:4326",
encoding="utf-8",
**kwargs,
):
"""Creates points for a CSV file and converts them to a vector dataset.
Args:
in_csv (str): The file path to the input CSV file.
output (str): The file path to the output vector dataset.
latitude (str, optional): The name of the column containing latitude coordinates. Defaults to "latitude".
longitude (str, optional): The name of the column containing longitude coordinates. Defaults to "longitude".
geometry (str, optional): The name of the column containing geometry. Defaults to None.
crs (str, optional): The coordinate reference system. Defaults to "EPSG:4326".
encoding (str, optional): The encoding of characters. Defaults to "utf-8".
**kwargs: Additional keyword arguments to pass to gdf.to_file().
"""
gdf = csv_to_gdf(in_csv, latitude, longitude, geometry, crs, encoding)
gdf.to_file(output, **kwargs)
d2s_tile(url, titiler_endpoint=None, **kwargs)
¶
Generate a D2S tile URL with optional API key.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
The base URL for the tile. |
required |
titiler_endpoint |
str |
The endpoint for the titiler service. Defaults to "https://tt.d2s.org". |
None |
**kwargs |
Any |
Additional keyword arguments to pass to the cog_stats function. |
{} |
Returns:
Type | Description |
---|---|
str |
The modified URL with the API key if required, otherwise the original URL. |
Exceptions:
Type | Description |
---|---|
ValueError |
If the API key is required but not set in the environment variables. |
Source code in leafmap/common.py
def d2s_tile(url: str, titiler_endpoint: str = None, **kwargs: Any) -> str:
"""Generate a D2S tile URL with optional API key.
Args:
url (str): The base URL for the tile.
titiler_endpoint (str, optional): The endpoint for the titiler service.
Defaults to "https://tt.d2s.org".
**kwargs (Any): Additional keyword arguments to pass to the cog_stats function.
Returns:
str: The modified URL with the API key if required, otherwise the original URL.
Raises:
ValueError: If the API key is required but not set in the environment variables.
"""
if titiler_endpoint is None:
titiler_endpoint = os.environ.get("TITILER_ENDPOINT", "https://titiler.xyz")
stats = cog_stats(url, titiler_endpoint=titiler_endpoint, **kwargs)
if "detail" in stats:
api_key = get_api_key("D2S_API_KEY")
if api_key is None:
raise ValueError("Please set the D2S_API_KEY environment variable.")
else:
return f"{url}?API_KEY={api_key}"
else:
return url
delete_shp(in_shp, verbose=False)
¶
Deletes a shapefile.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_shp |
str |
The input shapefile to delete. |
required |
verbose |
bool |
Whether to print out descriptive text. Defaults to True. |
False |
Source code in leafmap/common.py
def delete_shp(in_shp, verbose=False):
"""Deletes a shapefile.
Args:
in_shp (str): The input shapefile to delete.
verbose (bool, optional): Whether to print out descriptive text. Defaults to True.
"""
from pathlib import Path
in_shp = os.path.abspath(in_shp)
in_dir = os.path.dirname(in_shp)
basename = os.path.basename(in_shp).replace(".shp", "")
files = Path(in_dir).rglob(basename + ".*")
for file in files:
filepath = os.path.join(in_dir, str(file))
os.remove(filepath)
if verbose:
print(f"Deleted {filepath}")
df_to_gdf(df, geometry='geometry', src_crs='EPSG:4326', dst_crs=None, **kwargs)
¶
Converts a pandas DataFrame to a GeoPandas GeoDataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
df |
pandas.DataFrame |
The pandas DataFrame to convert. |
required |
geometry |
str |
The name of the geometry column in the DataFrame. |
'geometry' |
src_crs |
str |
The coordinate reference system (CRS) of the GeoDataFrame. Default is "EPSG:4326". |
'EPSG:4326' |
dst_crs |
str |
The target CRS of the GeoDataFrame. Default is None |
None |
Returns:
Type | Description |
---|---|
geopandas.GeoDataFrame |
The converted GeoPandas GeoDataFrame. |
Source code in leafmap/common.py
def df_to_gdf(df, geometry="geometry", src_crs="EPSG:4326", dst_crs=None, **kwargs):
"""
Converts a pandas DataFrame to a GeoPandas GeoDataFrame.
Args:
df (pandas.DataFrame): The pandas DataFrame to convert.
geometry (str): The name of the geometry column in the DataFrame.
src_crs (str): The coordinate reference system (CRS) of the GeoDataFrame. Default is "EPSG:4326".
dst_crs (str): The target CRS of the GeoDataFrame. Default is None
Returns:
geopandas.GeoDataFrame: The converted GeoPandas GeoDataFrame.
"""
import geopandas as gpd
from shapely import wkt
# Convert the geometry column to Shapely geometry objects
df[geometry] = df[geometry].apply(lambda x: wkt.loads(x))
# Convert the pandas DataFrame to a GeoPandas GeoDataFrame
gdf = gpd.GeoDataFrame(df, geometry=geometry, crs=src_crs, **kwargs)
if dst_crs is not None and dst_crs != src_crs:
gdf = gdf.to_crs(dst_crs)
return gdf
df_to_geojson(df, out_geojson=None, latitude='latitude', longitude='longitude', encoding='utf-8')
¶
Creates points for a Pandas DataFrame and exports data as a GeoJSON.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
df |
pandas.DataFrame |
The input Pandas DataFrame. |
required |
out_geojson |
str |
The file path to the exported GeoJSON. Default to None. |
None |
latitude |
str |
The name of the column containing latitude coordinates. Defaults to "latitude". |
'latitude' |
longitude |
str |
The name of the column containing longitude coordinates. Defaults to "longitude". |
'longitude' |
encoding |
str |
The encoding of characters. Defaults to "utf-8". |
'utf-8' |
Source code in leafmap/common.py
def df_to_geojson(
df,
out_geojson=None,
latitude="latitude",
longitude="longitude",
encoding="utf-8",
):
"""Creates points for a Pandas DataFrame and exports data as a GeoJSON.
Args:
df (pandas.DataFrame): The input Pandas DataFrame.
out_geojson (str): The file path to the exported GeoJSON. Default to None.
latitude (str, optional): The name of the column containing latitude coordinates. Defaults to "latitude".
longitude (str, optional): The name of the column containing longitude coordinates. Defaults to "longitude".
encoding (str, optional): The encoding of characters. Defaults to "utf-8".
"""
import json
from geojson import Feature, FeatureCollection, Point
if out_geojson is not None:
out_dir = os.path.dirname(os.path.abspath(out_geojson))
if not os.path.exists(out_dir):
os.makedirs(out_dir)
features = df.apply(
lambda row: Feature(
geometry=Point((float(row[longitude]), float(row[latitude]))),
properties=dict(row),
),
axis=1,
).tolist()
geojson = FeatureCollection(features=features)
if out_geojson is None:
return geojson
else:
with open(out_geojson, "w", encoding=encoding) as f:
f.write(json.dumps(geojson))
dict_to_json(data, file_path, indent=4)
¶
Writes a dictionary to a JSON file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
data |
dict |
A dictionary. |
required |
file_path |
str |
The path to the JSON file. |
required |
indent |
int |
The indentation of the JSON file. Defaults to 4. |
4 |
Exceptions:
Type | Description |
---|---|
TypeError |
If the input data is not a dictionary. |
Source code in leafmap/common.py
def dict_to_json(data, file_path, indent=4):
"""Writes a dictionary to a JSON file.
Args:
data (dict): A dictionary.
file_path (str): The path to the JSON file.
indent (int, optional): The indentation of the JSON file. Defaults to 4.
Raises:
TypeError: If the input data is not a dictionary.
"""
import json
file_path = check_file_path(file_path)
if isinstance(data, dict):
with open(file_path, "w") as f:
json.dump(data, f, indent=indent)
else:
raise TypeError("The provided data must be a dictionary.")
disjoint(input_features, selecting_features, output=None, **kwargs)
¶
Find the features in the input_features that do not intersect the selecting_features.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
input_features |
str | GeoDataFrame |
The input features to select from. Can be a file path or a GeoDataFrame. |
required |
selecting_features |
str | GeoDataFrame |
The features in the Input Features parameter will be selected based on their relationship to the features from this layer. |
required |
output |
are |
The output path to save the GeoDataFrame in a vector format (e.g., shapefile). Defaults to None. |
None |
Returns:
Type | Description |
---|---|
str | GeoDataFrame |
The path to the output file or the GeoDataFrame. |
Source code in leafmap/common.py
def disjoint(input_features, selecting_features, output=None, **kwargs):
"""Find the features in the input_features that do not intersect the selecting_features.
Args:
input_features (str | GeoDataFrame): The input features to select from. Can be a file path or a GeoDataFrame.
selecting_features (str | GeoDataFrame): The features in the Input Features parameter will be selected based
on their relationship to the features from this layer.
output (are, optional): The output path to save the GeoDataFrame in a vector format (e.g., shapefile). Defaults to None.
Returns:
str | GeoDataFrame: The path to the output file or the GeoDataFrame.
"""
import geopandas as gpd
if isinstance(input_features, str):
input_features = gpd.read_file(input_features, **kwargs)
elif not isinstance(input_features, gpd.GeoDataFrame):
raise TypeError("input_features must be a file path or a GeoDataFrame")
if isinstance(selecting_features, str):
selecting_features = gpd.read_file(selecting_features, **kwargs)
elif not isinstance(selecting_features, gpd.GeoDataFrame):
raise TypeError("selecting_features must be a file path or a GeoDataFrame")
selecting_features = selecting_features.to_crs(input_features.crs)
input_features["savedindex"] = input_features.index
intersecting = selecting_features.sjoin(input_features, how="inner")["savedindex"]
results = input_features[~input_features.savedindex.isin(intersecting)].drop(
columns=["savedindex"], axis=1
)
if output is not None:
results.to_file(output, **kwargs)
else:
return results
display_html(html, width='100%', height=500)
¶
Displays an HTML file or HTML string in a Jupyter Notebook.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
html |
Union[str, bytes] |
Path to an HTML file or an HTML string. |
required |
width |
str |
Width of the displayed iframe. Default is '100%'. |
'100%' |
height |
int |
Height of the displayed iframe. Default is 500. |
500 |
Returns:
Type | Description |
---|---|
None |
None |
Source code in leafmap/common.py
def display_html(
html: Union[str, bytes], width: str = "100%", height: int = 500
) -> None:
"""
Displays an HTML file or HTML string in a Jupyter Notebook.
Args:
html (Union[str, bytes]): Path to an HTML file or an HTML string.
width (str, optional): Width of the displayed iframe. Default is '100%'.
height (int, optional): Height of the displayed iframe. Default is 500.
Returns:
None
"""
from IPython.display import IFrame, display
if isinstance(html, str) and html.startswith("<"):
# If the input is an HTML string
html_content = html
elif isinstance(html, str):
# If the input is a file path
with open(html, "r") as file:
html_content = file.read()
elif isinstance(html, bytes):
# If the input is a byte string
html_content = html.decode("utf-8")
else:
raise ValueError("Invalid input type. Expected a file path or an HTML string.")
display(IFrame(src=html_content, width=width, height=height))
download_file(url=None, output=None, quiet=False, proxy=None, speed=None, use_cookies=True, verify=True, id=None, fuzzy=False, resume=False, unzip=True, overwrite=False, subfolder=False)
¶
Download a file from URL, including Google Drive shared URL.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
Google Drive URL is also supported. Defaults to None. |
None |
output |
str |
Output filename. Default is basename of URL. |
None |
quiet |
bool |
Suppress terminal output. Default is False. |
False |
proxy |
str |
Proxy. Defaults to None. |
None |
speed |
float |
Download byte size per second (e.g., 256KB/s = 256 * 1024). Defaults to None. |
None |
use_cookies |
bool |
Flag to use cookies. Defaults to True. |
True |
verify |
bool | str |
Either a bool, in which case it controls whether the server's TLS certificate is verified, or a string, in which case it must be a path to a CA bundle to use. Default is True.. Defaults to True. |
True |
id |
str |
Google Drive's file ID. Defaults to None. |
None |
fuzzy |
bool |
Fuzzy extraction of Google Drive's file Id. Defaults to False. |
False |
resume |
bool |
Resume the download from existing tmp file if possible. Defaults to False. |
False |
unzip |
bool |
Unzip the file. Defaults to True. |
True |
overwrite |
bool |
Overwrite the file if it already exists. Defaults to False. |
False |
subfolder |
bool |
Create a subfolder with the same name as the file. Defaults to False. |
False |
Returns:
Type | Description |
---|---|
str |
The output file path. |
Source code in leafmap/common.py
def download_file(
url=None,
output=None,
quiet=False,
proxy=None,
speed=None,
use_cookies=True,
verify=True,
id=None,
fuzzy=False,
resume=False,
unzip=True,
overwrite=False,
subfolder=False,
):
"""Download a file from URL, including Google Drive shared URL.
Args:
url (str, optional): Google Drive URL is also supported. Defaults to None.
output (str, optional): Output filename. Default is basename of URL.
quiet (bool, optional): Suppress terminal output. Default is False.
proxy (str, optional): Proxy. Defaults to None.
speed (float, optional): Download byte size per second (e.g., 256KB/s = 256 * 1024). Defaults to None.
use_cookies (bool, optional): Flag to use cookies. Defaults to True.
verify (bool | str, optional): Either a bool, in which case it controls whether the server's TLS certificate is verified, or a string,
in which case it must be a path to a CA bundle to use. Default is True.. Defaults to True.
id (str, optional): Google Drive's file ID. Defaults to None.
fuzzy (bool, optional): Fuzzy extraction of Google Drive's file Id. Defaults to False.
resume (bool, optional): Resume the download from existing tmp file if possible. Defaults to False.
unzip (bool, optional): Unzip the file. Defaults to True.
overwrite (bool, optional): Overwrite the file if it already exists. Defaults to False.
subfolder (bool, optional): Create a subfolder with the same name as the file. Defaults to False.
Returns:
str: The output file path.
"""
try:
import gdown
except ImportError:
print(
"The gdown package is required for this function. Use `pip install gdown` to install it."
)
return
if output is None:
if isinstance(url, str) and url.startswith("http"):
output = os.path.basename(url)
out_dir = os.path.abspath(os.path.dirname(output))
if not os.path.exists(out_dir):
os.makedirs(out_dir)
if isinstance(url, str):
if os.path.exists(os.path.abspath(output)) and (not overwrite):
print(
f"{output} already exists. Skip downloading. Set overwrite=True to overwrite."
)
return os.path.abspath(output)
else:
url = github_raw_url(url)
if "https://drive.google.com/file/d/" in url:
fuzzy = True
output = gdown.download(
url, output, quiet, proxy, speed, use_cookies, verify, id, fuzzy, resume
)
if unzip:
if output.endswith(".zip"):
with zipfile.ZipFile(output, "r") as zip_ref:
if not quiet:
print("Extracting files...")
if subfolder:
basename = os.path.splitext(os.path.basename(output))[0]
output = os.path.join(out_dir, basename)
if not os.path.exists(output):
os.makedirs(output)
zip_ref.extractall(output)
else:
zip_ref.extractall(os.path.dirname(output))
elif output.endswith(".tar.gz") or output.endswith(".tar"):
if output.endswith(".tar.gz"):
mode = "r:gz"
else:
mode = "r"
with tarfile.open(output, mode) as tar_ref:
if not quiet:
print("Extracting files...")
if subfolder:
basename = os.path.splitext(os.path.basename(output))[0]
output = os.path.join(out_dir, basename)
if not os.path.exists(output):
os.makedirs(output)
tar_ref.extractall(output)
else:
tar_ref.extractall(os.path.dirname(output))
return os.path.abspath(output)
download_file_lite(url, output=None, binary=False, overwrite=False, **kwargs)
async
¶
Download a file using Pyodide. This function is only available on JupyterLite. Call the function with await, such as await download_file_lite(url).
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
The URL of the file. |
required |
output |
str |
The local path to save the file. Defaults to None. |
None |
binary |
bool |
Whether the file is binary. Defaults to False. |
False |
overwrite |
bool |
Whether to overwrite the file if it exists. Defaults to False. |
False |
Source code in leafmap/common.py
async def download_file_lite(url, output=None, binary=False, overwrite=False, **kwargs):
"""Download a file using Pyodide. This function is only available on JupyterLite. Call the function with await, such as await download_file_lite(url).
Args:
url (str): The URL of the file.
output (str, optional): The local path to save the file. Defaults to None.
binary (bool, optional): Whether the file is binary. Defaults to False.
overwrite (bool, optional): Whether to overwrite the file if it exists. Defaults to False.
"""
import sys
import pyodide # pylint: disable=E0401
if "pyodide" not in sys.modules:
raise ValueError("Pyodide is not available.")
if output is None:
output = os.path.basename(url)
output = os.path.abspath(output)
ext = os.path.splitext(output)[1]
if ext in [".png", "jpg", ".tif", ".tiff", "zip", "gz", "bz2", "xz"]:
binary = True
if os.path.exists(output) and not overwrite:
print(f"{output} already exists, skip downloading.")
return output
if binary:
response = await pyodide.http.pyfetch(url)
with open(output, "wb") as f:
f.write(await response.bytes())
else:
obj = pyodide.http.open_url(url)
with open(output, "w") as fd:
shutil.copyfileobj(obj, fd)
return output
download_files(urls, out_dir=None, filenames=None, quiet=False, proxy=None, speed=None, use_cookies=True, verify=True, id=None, fuzzy=False, resume=False, unzip=True, overwrite=False, subfolder=False, multi_part=False)
¶
Download files from URLs, including Google Drive shared URL.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
urls |
list |
The list of urls to download. Google Drive URL is also supported. |
required |
out_dir |
str |
The output directory. Defaults to None. |
None |
filenames |
list |
Output filename. Default is basename of URL. |
None |
quiet |
bool |
Suppress terminal output. Default is False. |
False |
proxy |
str |
Proxy. Defaults to None. |
None |
speed |
float |
Download byte size per second (e.g., 256KB/s = 256 * 1024). Defaults to None. |
None |
use_cookies |
bool |
Flag to use cookies. Defaults to True. |
True |
verify |
bool | str |
Either a bool, in which case it controls whether the server's TLS certificate is verified, or a string, in which case it must be a path to a CA bundle to use. Default is True.. Defaults to True. |
True |
id |
str |
Google Drive's file ID. Defaults to None. |
None |
fuzzy |
bool |
Fuzzy extraction of Google Drive's file Id. Defaults to False. |
False |
resume |
bool |
Resume the download from existing tmp file if possible. Defaults to False. |
False |
unzip |
bool |
Unzip the file. Defaults to True. |
True |
overwrite |
bool |
Overwrite the file if it already exists. Defaults to False. |
False |
subfolder |
bool |
Create a subfolder with the same name as the file. Defaults to False. |
False |
multi_part |
bool |
If the file is a multi-part file. Defaults to False. |
False |
Examples:
files = ["sam_hq_vit_tiny.zip", "sam_hq_vit_tiny.z01", "sam_hq_vit_tiny.z02", "sam_hq_vit_tiny.z03"] base_url = "https://github.com/opengeos/datasets/releases/download/models/" urls = [base_url + f for f in files] leafmap.download_files(urls, out_dir="models", multi_part=True)
Source code in leafmap/common.py
def download_files(
urls,
out_dir=None,
filenames=None,
quiet=False,
proxy=None,
speed=None,
use_cookies=True,
verify=True,
id=None,
fuzzy=False,
resume=False,
unzip=True,
overwrite=False,
subfolder=False,
multi_part=False,
):
"""Download files from URLs, including Google Drive shared URL.
Args:
urls (list): The list of urls to download. Google Drive URL is also supported.
out_dir (str, optional): The output directory. Defaults to None.
filenames (list, optional): Output filename. Default is basename of URL.
quiet (bool, optional): Suppress terminal output. Default is False.
proxy (str, optional): Proxy. Defaults to None.
speed (float, optional): Download byte size per second (e.g., 256KB/s = 256 * 1024). Defaults to None.
use_cookies (bool, optional): Flag to use cookies. Defaults to True.
verify (bool | str, optional): Either a bool, in which case it controls whether the server's TLS certificate is verified, or a string, in which case it must be a path to a CA bundle to use. Default is True.. Defaults to True.
id (str, optional): Google Drive's file ID. Defaults to None.
fuzzy (bool, optional): Fuzzy extraction of Google Drive's file Id. Defaults to False.
resume (bool, optional): Resume the download from existing tmp file if possible. Defaults to False.
unzip (bool, optional): Unzip the file. Defaults to True.
overwrite (bool, optional): Overwrite the file if it already exists. Defaults to False.
subfolder (bool, optional): Create a subfolder with the same name as the file. Defaults to False.
multi_part (bool, optional): If the file is a multi-part file. Defaults to False.
Examples:
files = ["sam_hq_vit_tiny.zip", "sam_hq_vit_tiny.z01", "sam_hq_vit_tiny.z02", "sam_hq_vit_tiny.z03"]
base_url = "https://github.com/opengeos/datasets/releases/download/models/"
urls = [base_url + f for f in files]
leafmap.download_files(urls, out_dir="models", multi_part=True)
"""
if out_dir is None:
out_dir = os.getcwd()
if filenames is None:
filenames = [None] * len(urls)
filepaths = []
for url, output in zip(urls, filenames):
if output is None:
filename = os.path.join(out_dir, os.path.basename(url))
else:
filename = os.path.join(out_dir, output)
filepaths.append(filename)
if multi_part:
unzip = False
download_file(
url,
filename,
quiet,
proxy,
speed,
use_cookies,
verify,
id,
fuzzy,
resume,
unzip,
overwrite,
subfolder,
)
if multi_part:
archive = os.path.splitext(filename)[0] + ".zip"
out_dir = os.path.dirname(filename)
extract_archive(archive, out_dir)
for file in filepaths:
os.remove(file)
download_folder(url=None, id=None, output=None, quiet=False, proxy=None, speed=None, use_cookies=True, remaining_ok=False)
¶
Downloads the entire folder from URL.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
URL of the Google Drive folder. Must be of the format 'https://drive.google.com/drive/folders/{url}'. Defaults to None. |
None |
id |
str |
Google Drive's folder ID. Defaults to None. |
None |
output |
str |
String containing the path of the output folder. Defaults to current working directory. |
None |
quiet |
bool |
Suppress terminal output. Defaults to False. |
False |
proxy |
str |
Proxy. Defaults to None. |
None |
speed |
float |
Download byte size per second (e.g., 256KB/s = 256 * 1024). Defaults to None. |
None |
use_cookies |
bool |
Flag to use cookies. Defaults to True. |
True |
resume |
bool |
Resume the download from existing tmp file if possible. Defaults to False. |
required |
Returns:
Type | Description |
---|---|
list |
List of files downloaded, or None if failed. |
Source code in leafmap/common.py
def download_folder(
url=None,
id=None,
output=None,
quiet=False,
proxy=None,
speed=None,
use_cookies=True,
remaining_ok=False,
):
"""Downloads the entire folder from URL.
Args:
url (str, optional): URL of the Google Drive folder. Must be of the format 'https://drive.google.com/drive/folders/{url}'. Defaults to None.
id (str, optional): Google Drive's folder ID. Defaults to None.
output (str, optional): String containing the path of the output folder. Defaults to current working directory.
quiet (bool, optional): Suppress terminal output. Defaults to False.
proxy (str, optional): Proxy. Defaults to None.
speed (float, optional): Download byte size per second (e.g., 256KB/s = 256 * 1024). Defaults to None.
use_cookies (bool, optional): Flag to use cookies. Defaults to True.
resume (bool, optional): Resume the download from existing tmp file if possible. Defaults to False.
Returns:
list: List of files downloaded, or None if failed.
"""
try:
import gdown
except ImportError:
print(
"The gdown package is required for this function. Use `pip install gdown` to install it."
)
return
files = gdown.download_folder(
url, id, output, quiet, proxy, speed, use_cookies, remaining_ok
)
return files
download_from_url(url, out_file_name=None, out_dir='.', unzip=True, verbose=True)
¶
Download a file from a URL (e.g., https://github.com/opengeos/whitebox-python/raw/master/examples/testdata.zip)
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
The HTTP URL to download. |
required |
out_file_name |
str |
The output file name to use. Defaults to None. |
None |
out_dir |
str |
The output directory to use. Defaults to '.'. |
'.' |
unzip |
bool |
Whether to unzip the downloaded file if it is a zip file. Defaults to True. |
True |
verbose |
bool |
Whether to display or not the output of the function |
True |
Source code in leafmap/common.py
def download_from_url(
url: str,
out_file_name: Optional[str] = None,
out_dir: Optional[str] = ".",
unzip: Optional[bool] = True,
verbose: Optional[bool] = True,
):
"""Download a file from a URL (e.g., https://github.com/opengeos/whitebox-python/raw/master/examples/testdata.zip)
Args:
url (str): The HTTP URL to download.
out_file_name (str, optional): The output file name to use. Defaults to None.
out_dir (str, optional): The output directory to use. Defaults to '.'.
unzip (bool, optional): Whether to unzip the downloaded file if it is a zip file. Defaults to True.
verbose (bool, optional): Whether to display or not the output of the function
"""
in_file_name = os.path.basename(url)
out_dir = check_dir(out_dir)
if out_file_name is None:
out_file_name = in_file_name
out_file_path = os.path.join(out_dir, out_file_name)
if verbose:
print("Downloading {} ...".format(url))
try:
urllib.request.urlretrieve(url, out_file_path)
except Exception:
raise Exception("The URL is invalid. Please double check the URL.")
final_path = out_file_path
if unzip:
# if it is a zip file
if ".zip" in out_file_name:
if verbose:
print("Unzipping {} ...".format(out_file_name))
with zipfile.ZipFile(out_file_path, "r") as zip_ref:
zip_ref.extractall(out_dir)
final_path = os.path.join(
os.path.abspath(out_dir), out_file_name.replace(".zip", "")
)
# if it is a tar file
if ".tar" in out_file_name:
if verbose:
print("Unzipping {} ...".format(out_file_name))
with tarfile.open(out_file_path, "r") as tar_ref:
with tarfile.open(out_file_path, "r") as tar_ref:
def is_within_directory(directory, target):
abs_directory = os.path.abspath(directory)
abs_target = os.path.abspath(target)
prefix = os.path.commonprefix([abs_directory, abs_target])
return prefix == abs_directory
def safe_extract(
tar, path=".", members=None, *, numeric_owner=False
):
for member in tar.getmembers():
member_path = os.path.join(path, member.name)
if not is_within_directory(path, member_path):
raise Exception("Attempted Path Traversal in Tar File")
tar.extractall(path, members, numeric_owner=numeric_owner)
safe_extract(tar_ref, out_dir)
final_path = os.path.join(
os.path.abspath(out_dir), out_file_name.replace(".tart", "")
)
if verbose:
print("Data downloaded to: {}".format(final_path))
download_google_buildings(location, out_dir=None, merge_output=None, head=None, keep_geojson=False, overwrite=False, quiet=False, **kwargs)
¶
Download Google Open Building dataset for a specific location. Check the dataset links from https://sites.research.google/open-buildings.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
location |
str |
The location name for which to download the dataset. |
required |
out_dir |
Optional[str] |
The output directory to save the downloaded files. If not provided, the current working directory is used. |
None |
merge_output |
Optional[str] |
Optional. The output file path for merging the downloaded files into a single GeoDataFrame. |
None |
head |
Optional[int] |
Optional. The number of files to download. If not provided, all files will be downloaded. |
None |
keep_geojson |
bool |
Optional. If True, the GeoJSON files will be kept after converting them to CSV files. |
False |
overwrite |
bool |
Optional. If True, overwrite the existing files. |
False |
quiet |
bool |
Optional. If True, suppresses the download progress messages. |
False |
**kwargs |
Additional keyword arguments to be passed to the |
{} |
Returns:
Type | Description |
---|---|
List[str] |
A list of file paths of the downloaded files. |
Source code in leafmap/common.py
def download_google_buildings(
location: str,
out_dir: Optional[str] = None,
merge_output: Optional[str] = None,
head: Optional[int] = None,
keep_geojson: bool = False,
overwrite: bool = False,
quiet: bool = False,
**kwargs,
) -> List[str]:
"""
Download Google Open Building dataset for a specific location. Check the dataset links from
https://sites.research.google/open-buildings.
Args:
location: The location name for which to download the dataset.
out_dir: The output directory to save the downloaded files. If not provided, the current working directory is used.
merge_output: Optional. The output file path for merging the downloaded files into a single GeoDataFrame.
head: Optional. The number of files to download. If not provided, all files will be downloaded.
keep_geojson: Optional. If True, the GeoJSON files will be kept after converting them to CSV files.
overwrite: Optional. If True, overwrite the existing files.
quiet: Optional. If True, suppresses the download progress messages.
**kwargs: Additional keyword arguments to be passed to the `gpd.to_file` function.
Returns:
A list of file paths of the downloaded files.
"""
import pandas as pd
import geopandas as gpd
from shapely import wkt
building_url = "https://sites.research.google/open-buildings/tiles.geojson"
country_url = (
"https://naciscdn.org/naturalearth/110m/cultural/ne_110m_admin_0_countries.zip"
)
if out_dir is None:
out_dir = os.getcwd()
if not os.path.exists(out_dir):
os.makedirs(out_dir)
building_gdf = gpd.read_file(building_url)
country_gdf = gpd.read_file(country_url)
country = country_gdf[country_gdf["NAME"] == location]
if len(country) == 0:
country = country_gdf[country_gdf["NAME_LONG"] == location]
if len(country) == 0:
raise ValueError(f"Could not find {location} in the Natural Earth dataset.")
gdf = building_gdf[building_gdf.intersects(country.geometry.iloc[0])]
gdf.sort_values(by="size_mb", inplace=True)
print(f"Found {len(gdf)} links for {location}.")
if head is not None:
gdf = gdf.head(head)
if len(gdf) > 0:
links = gdf["tile_url"].tolist()
download_files(links, out_dir=out_dir, quiet=quiet, **kwargs)
filenames = [os.path.join(out_dir, os.path.basename(link)) for link in links]
gdfs = []
for filename in filenames:
# Read the CSV file into a pandas DataFrame
df = pd.read_csv(filename)
# Create a geometry column from the "geometry" column in the DataFrame
df["geometry"] = df["geometry"].apply(wkt.loads)
# Convert the pandas DataFrame to a GeoDataFrame
gdf = gpd.GeoDataFrame(df, geometry="geometry")
gdf.crs = "EPSG:4326"
if keep_geojson:
gdf.to_file(
filename.replace(".csv.gz", ".geojson"), driver="GeoJSON", **kwargs
)
gdfs.append(gdf)
if merge_output:
if os.path.exists(merge_output) and not overwrite:
print(f"File {merge_output} already exists, skip merging...")
else:
if not quiet:
print("Merging GeoDataFrames ...")
gdf = gpd.GeoDataFrame(
pd.concat(gdfs, ignore_index=True), crs="EPSG:4326"
)
gdf.to_file(merge_output, **kwargs)
else:
print(f"No buildings found for {location}.")
download_ms_buildings(location, out_dir=None, merge_output=None, head=None, quiet=False, **kwargs)
¶
Download Microsoft Buildings dataset for a specific location. Check the dataset links from https://minedbuildings.blob.core.windows.net/global-buildings/dataset-links.csv.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
location |
str |
The location name for which to download the dataset. |
required |
out_dir |
Optional[str] |
The output directory to save the downloaded files. If not provided, the current working directory is used. |
None |
merge_output |
Optional[str] |
Optional. The output file path for merging the downloaded files into a single GeoDataFrame. |
None |
head |
Optional. The number of files to download. If not provided, all files will be downloaded. |
None |
|
quiet |
bool |
Optional. If True, suppresses the download progress messages. |
False |
**kwargs |
Additional keyword arguments to be passed to the |
{} |
Returns:
Type | Description |
---|---|
List[str] |
A list of file paths of the downloaded files. |
Source code in leafmap/common.py
def download_ms_buildings(
location: str,
out_dir: Optional[str] = None,
merge_output: Optional[str] = None,
head=None,
quiet: bool = False,
**kwargs,
) -> List[str]:
"""
Download Microsoft Buildings dataset for a specific location. Check the dataset links from
https://minedbuildings.blob.core.windows.net/global-buildings/dataset-links.csv.
Args:
location: The location name for which to download the dataset.
out_dir: The output directory to save the downloaded files. If not provided, the current working directory is used.
merge_output: Optional. The output file path for merging the downloaded files into a single GeoDataFrame.
head: Optional. The number of files to download. If not provided, all files will be downloaded.
quiet: Optional. If True, suppresses the download progress messages.
**kwargs: Additional keyword arguments to be passed to the `gpd.to_file` function.
Returns:
A list of file paths of the downloaded files.
"""
import pandas as pd
import geopandas as gpd
from shapely.geometry import shape
if out_dir is None:
out_dir = os.getcwd()
if not os.path.exists(out_dir):
os.makedirs(out_dir)
dataset_links = pd.read_csv(
"https://minedbuildings.blob.core.windows.net/global-buildings/dataset-links.csv"
)
country_links = dataset_links[dataset_links.Location == location]
if not quiet:
print(f"Found {len(country_links)} links for {location}")
if head is not None:
country_links = country_links.head(head)
filenames = []
i = 1
for _, row in country_links.iterrows():
if not quiet:
print(f"Downloading {i} of {len(country_links)}: {row.QuadKey}.geojson")
i += 1
filename = os.path.join(out_dir, f"{row.QuadKey}.geojson")
filenames.append(filename)
if os.path.exists(filename):
print(f"File {filename} already exists, skipping...")
continue
df = pd.read_json(row.Url, lines=True)
df["geometry"] = df["geometry"].apply(shape)
gdf = gpd.GeoDataFrame(df, crs=4326)
gdf.to_file(filename, driver="GeoJSON", **kwargs)
if merge_output is not None:
if os.path.exists(merge_output):
print(f"File {merge_output} already exists, skip merging...")
return filenames
merge_vector(filenames, merge_output, quiet=quiet)
return filenames
download_ned(region, out_dir=None, return_url=False, download_args={}, geopandas_args={}, query={})
¶
Download the US National Elevation Datasets (NED) for a region.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
region |
str | list |
A filepath to a vector dataset or a list of bounds in the form of [minx, miny, maxx, maxy]. |
required |
out_dir |
str |
The directory to download the files to. Defaults to None, which uses the current working directory. |
None |
return_url |
bool |
Whether to return the download URLs of the files. Defaults to False. |
False |
download_args |
dict |
A dictionary of arguments to pass to the download_file function. Defaults to {}. |
{} |
geopandas_args |
dict |
A dictionary of arguments to pass to the geopandas.read_file() function. Used for reading a region URL|filepath. |
{} |
query |
dict |
A dictionary of arguments to pass to the The_national_map_USGS.find_details() function. See https://apps.nationalmap.gov/tnmaccess/#/product for more information. |
{} |
Returns:
Type | Description |
---|---|
list |
A list of the download URLs of the files if return_url is True. |
Source code in leafmap/common.py
def download_ned(
region,
out_dir=None,
return_url=False,
download_args={},
geopandas_args={},
query={},
) -> Union[None, List]:
"""Download the US National Elevation Datasets (NED) for a region.
Args:
region (str | list): A filepath to a vector dataset or a list of bounds in the form of [minx, miny, maxx, maxy].
out_dir (str, optional): The directory to download the files to. Defaults to None, which uses the current working directory.
return_url (bool, optional): Whether to return the download URLs of the files. Defaults to False.
download_args (dict, optional): A dictionary of arguments to pass to the download_file function. Defaults to {}.
geopandas_args (dict, optional): A dictionary of arguments to pass to the geopandas.read_file() function.
Used for reading a region URL|filepath.
query (dict, optional): A dictionary of arguments to pass to the The_national_map_USGS.find_details() function.
See https://apps.nationalmap.gov/tnmaccess/#/product for more information.
Returns:
list: A list of the download URLs of the files if return_url is True.
"""
if os.environ.get("USE_MKDOCS") is not None:
return
if not query:
query = {
"datasets": "National Elevation Dataset (NED) 1/3 arc-second",
"prodFormats": "GeoTIFF",
}
TNM = The_national_map_USGS()
if return_url:
return TNM.find_tiles(region=region, geopandas_args=geopandas_args, API=query)
return TNM.download_tiles(
region=region,
out_dir=out_dir,
download_args=download_args,
geopandas_args=geopandas_args,
API=query,
)
download_tnm(region=None, out_dir=None, return_url=False, download_args={}, geopandas_args={}, API={})
¶
Download the US National Elevation Datasets (NED) for a region.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
region |
str | list |
An URL|filepath to a vector dataset Or a list of bounds in the form of [minx, miny, maxx, maxy]. Alternatively you could use API parameters such as polygon or bbox. |
None |
out_dir |
str |
The directory to download the files to. Defaults to None, which uses the current working directory. |
None |
return_url |
bool |
Whether to return the download URLs of the files. Defaults to False. |
False |
download_args |
dict |
A dictionary of arguments to pass to the download_file function. Defaults to {}. |
{} |
geopandas_args |
dict |
A dictionary of arguments to pass to the geopandas.read_file() function. Used for reading a region URL|filepath. |
{} |
API |
dict |
A dictionary of arguments to pass to the The_national_map_USGS.find_details() function. Exposes most of the documented API. Defaults to {} |
{} |
Returns:
Type | Description |
---|---|
list |
A list of the download URLs of the files if return_url is True. |
Source code in leafmap/common.py
def download_tnm(
region=None,
out_dir=None,
return_url=False,
download_args={},
geopandas_args={},
API={},
) -> Union[None, List]:
"""Download the US National Elevation Datasets (NED) for a region.
Args:
region (str | list, optional): An URL|filepath to a vector dataset Or a list of bounds in the form of [minx, miny, maxx, maxy].
Alternatively you could use API parameters such as polygon or bbox.
out_dir (str, optional): The directory to download the files to. Defaults to None, which uses the current working directory.
return_url (bool, optional): Whether to return the download URLs of the files. Defaults to False.
download_args (dict, optional): A dictionary of arguments to pass to the download_file function. Defaults to {}.
geopandas_args (dict, optional): A dictionary of arguments to pass to the geopandas.read_file() function.
Used for reading a region URL|filepath.
API (dict, optional): A dictionary of arguments to pass to the The_national_map_USGS.find_details() function.
Exposes most of the documented API. Defaults to {}
Returns:
list: A list of the download URLs of the files if return_url is True.
"""
if os.environ.get("USE_MKDOCS") is not None:
return
TNM = The_national_map_USGS()
if return_url:
return TNM.find_tiles(region=region, geopandas_args=geopandas_args, API=API)
return TNM.download_tiles(
region=region,
out_dir=out_dir,
download_args=download_args,
geopandas_args=geopandas_args,
API=API,
)
edit_download_html(htmlWidget, filename, title='Click here to download: ')
¶
Downloads a file from voila. Adopted from https://github.com/voila-dashboards/voila/issues/578#issuecomment-617668058
Parameters:
Name | Type | Description | Default |
---|---|---|---|
htmlWidget |
object |
The HTML widget to display the URL. |
required |
filename |
str |
File path to download. |
required |
title |
str |
Download description. Defaults to "Click here to download: ". |
'Click here to download: ' |
Source code in leafmap/common.py
def edit_download_html(htmlWidget, filename, title="Click here to download: "):
"""Downloads a file from voila. Adopted from https://github.com/voila-dashboards/voila/issues/578#issuecomment-617668058
Args:
htmlWidget (object): The HTML widget to display the URL.
filename (str): File path to download.
title (str, optional): Download description. Defaults to "Click here to download: ".
"""
# from IPython.display import HTML
# import ipywidgets as widgets
import base64
# Change widget html temporarily to a font-awesome spinner
htmlWidget.value = '<i class="fa fa-spinner fa-spin fa-2x fa-fw"></i><span class="sr-only">Loading...</span>'
# Process raw data
data = open(filename, "rb").read()
b64 = base64.b64encode(data)
payload = b64.decode()
basename = os.path.basename(filename)
# Create and assign html to widget
html = '<a download="{filename}" href="data:text/csv;base64,{payload}" target="_blank">{title}</a>'
htmlWidget.value = html.format(
payload=payload, title=title + basename, filename=basename
)
ee_tile_url(ee_object=None, vis_params={}, asset_id=None, ee_initialize=False, project_id=None, **kwargs)
¶
Adds a Google Earth Engine tile layer to the map based on the tile layer URL from https://github.com/opengeos/ee-tile-layers/blob/main/datasets.tsv.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
ee_object |
object |
The Earth Engine object to display. |
None |
vis_params |
dict |
Visualization parameters. For example, {'min': 0, 'max': 100}. |
{} |
asset_id |
str |
The ID of the Earth Engine asset. |
None |
ee_initialize |
bool |
Whether to initialize the Earth Engine |
False |
Returns:
Type | Description |
---|---|
None |
None |
Source code in leafmap/common.py
def ee_tile_url(
ee_object=None,
vis_params={},
asset_id: str = None,
ee_initialize: bool = False,
project_id=None,
**kwargs,
) -> None:
"""
Adds a Google Earth Engine tile layer to the map based on the tile layer URL from
https://github.com/opengeos/ee-tile-layers/blob/main/datasets.tsv.
Args:
ee_object (object): The Earth Engine object to display.
vis_params (dict): Visualization parameters. For example, {'min': 0, 'max': 100}.
asset_id (str): The ID of the Earth Engine asset.
ee_initialize (bool, optional): Whether to initialize the Earth Engine
Returns:
None
"""
import pandas as pd
if isinstance(asset_id, str):
df = pd.read_csv(
"https://raw.githubusercontent.com/opengeos/ee-tile-layers/main/datasets.tsv",
sep="\t",
)
asset_id = asset_id.strip()
if asset_id in df["id"].values:
url = df.loc[df["id"] == asset_id, "url"].values[0]
return url
else:
print(f"The provided EE tile layer {asset_id} does not exist.")
return None
elif ee_object is not None:
try:
import geemap
from geemap.ee_tile_layers import _get_tile_url_format
if ee_initialize:
geemap.ee_initialize(project=project_id, **kwargs)
url = _get_tile_url_format(ee_object, vis_params)
return url
except Exception as e:
print(e)
return None
execute_maplibre_notebook_dir(in_dir, out_dir, delete_html=True, replace_api_key=True, recursive=False, keep_notebook=False, index_html=True)
¶
Executes Jupyter notebooks found in a specified directory, optionally replacing API keys and deleting HTML outputs.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_dir |
str |
The input directory containing Jupyter notebooks to be executed. |
required |
out_dir |
str |
The output directory where the executed notebooks and their HTML outputs will be saved. |
required |
delete_html |
bool |
If True, deletes any existing HTML files in the output directory before execution. Defaults to True. |
True |
replace_api_key |
bool |
If True, replaces the API key in the output HTML. Defaults to True. set "MAPTILER_KEY" and "MAPTILER_KEY_PUBLIC" to your MapTiler API key and public key, respectively. |
True |
recursive |
bool |
If True, searches for notebooks in the input directory recursively. Defaults to False. |
False |
keep_notebook |
bool |
If True, keeps the executed notebooks in the output directory. Defaults to False. |
False |
index_html |
bool |
If True, generates an index.html file in the output directory listing all files. Defaults to True. |
True |
Returns:
Type | Description |
---|---|
None |
None |
Source code in leafmap/common.py
def execute_maplibre_notebook_dir(
in_dir: str,
out_dir: str,
delete_html: bool = True,
replace_api_key: bool = True,
recursive: bool = False,
keep_notebook: bool = False,
index_html: bool = True,
) -> None:
"""
Executes Jupyter notebooks found in a specified directory, optionally replacing API keys and deleting HTML outputs.
Args:
in_dir (str): The input directory containing Jupyter notebooks to be executed.
out_dir (str): The output directory where the executed notebooks and their HTML outputs will be saved.
delete_html (bool, optional): If True, deletes any existing HTML files in the output directory before execution. Defaults to True.
replace_api_key (bool, optional): If True, replaces the API key in the output HTML. Defaults to True.
set "MAPTILER_KEY" and "MAPTILER_KEY_PUBLIC" to your MapTiler API key and public key, respectively.
recursive (bool, optional): If True, searches for notebooks in the input directory recursively. Defaults to False.
keep_notebook (bool, optional): If True, keeps the executed notebooks in the output directory. Defaults to False.
index_html (bool, optional): If True, generates an index.html file in the output directory listing all files. Defaults to True.
Returns:
None
"""
import shutil
if not os.path.exists(out_dir):
os.makedirs(out_dir)
if replace_api_key:
os.environ["MAPTILER_REPLACE_KEY"] = "True"
if delete_html:
html_files = find_files(out_dir, "*.html", recursive=recursive)
for file in html_files:
os.remove(file)
files = find_files(in_dir, "*.ipynb", recursive=recursive)
for index, file in enumerate(files):
print(f"Processing {index + 1}/{len(files)}: {file} ...")
basename = os.path.basename(file)
out_file = os.path.join(out_dir, basename)
shutil.copy(file, out_file)
with open(out_file, "r") as f:
lines = f.readlines()
out_lines = []
for line in lines:
if line.strip() == '"m"':
out_lines.append(line.replace("m", "m.to_html()"))
else:
out_lines.append(line)
with open(out_file, "w") as f:
f.writelines(out_lines)
out_html = os.path.basename(out_file).replace(".ipynb", ".html")
os.environ["MAPLIBRE_OUTPUT"] = out_html
execute_notebook(out_file)
if not keep_notebook:
all_files = find_files(out_dir, "*", recursive=recursive)
for file in all_files:
if not file.endswith(".html"):
os.remove(file)
if index_html:
generate_index_html(out_dir)
execute_notebook(in_file)
¶
Executes a Jupyter notebook and save output cells
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_file |
str |
Input Jupyter notebook. |
required |
Source code in leafmap/common.py
def execute_notebook(in_file):
"""Executes a Jupyter notebook and save output cells
Args:
in_file (str): Input Jupyter notebook.
"""
# command = 'jupyter nbconvert --to notebook --execute ' + in_file + ' --inplace'
command = 'jupyter nbconvert --to notebook --execute "{}" --inplace'.format(in_file)
print(os.popen(command).read().rstrip())
# os.popen(command)
execute_notebook_dir(in_dir)
¶
Executes all Jupyter notebooks in the given directory recursively and save output cells.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_dir |
str |
Input folder containing notebooks. |
required |
Source code in leafmap/common.py
def execute_notebook_dir(in_dir):
"""Executes all Jupyter notebooks in the given directory recursively and save output cells.
Args:
in_dir (str): Input folder containing notebooks.
"""
from pathlib import Path
in_dir = os.path.abspath(in_dir)
files = list(Path(in_dir).rglob("*.ipynb"))
files.sort()
count = len(files)
if files is not None:
for index, file in enumerate(files):
in_file = str(file)
print(f"Processing {index + 1}/{count}: {file} ...")
execute_notebook(in_file)
explode(coords)
¶
Explode a GeoJSON geometry's coordinates object and yield coordinate tuples. As long as the input is conforming, the type of the geometry doesn't matter. From Fiona 1.4.8
Parameters:
Name | Type | Description | Default |
---|---|---|---|
coords |
list |
A list of coordinates. |
required |
Yields:
Type | Description |
---|---|
[type] |
[description] |
Source code in leafmap/common.py
def explode(coords):
"""Explode a GeoJSON geometry's coordinates object and yield
coordinate tuples. As long as the input is conforming, the type of
the geometry doesn't matter. From Fiona 1.4.8
Args:
coords (list): A list of coordinates.
Yields:
[type]: [description]
"""
for e in coords:
if isinstance(e, (float, int)):
yield coords
break
else:
for f in explode(e):
yield f
extract_archive(archive, outdir=None, **kwargs)
¶
Extracts a multipart archive.
This function uses the patoolib library to extract a multipart archive. If the patoolib library is not installed, it attempts to install it. If the archive does not end with ".zip", it appends ".zip" to the archive name. If the extraction fails (for example, if the files already exist), it skips the extraction.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
archive |
str |
The path to the archive file. |
required |
outdir |
str |
The directory where the archive should be extracted. |
None |
**kwargs |
Arbitrary keyword arguments for the patoolib.extract_archive function. |
{} |
Returns:
Type | Description |
---|---|
None |
None |
Exceptions:
Type | Description |
---|---|
Exception |
An exception is raised if the extraction fails for reasons other than the files already existing. |
Examples:
files = ["sam_hq_vit_tiny.zip", "sam_hq_vit_tiny.z01", "sam_hq_vit_tiny.z02", "sam_hq_vit_tiny.z03"] base_url = "https://github.com/opengeos/datasets/releases/download/models/" urls = [base_url + f for f in files] leafmap.download_files(urls, out_dir="models", multi_part=True)
Source code in leafmap/common.py
def extract_archive(archive, outdir=None, **kwargs) -> None:
"""
Extracts a multipart archive.
This function uses the patoolib library to extract a multipart archive.
If the patoolib library is not installed, it attempts to install it.
If the archive does not end with ".zip", it appends ".zip" to the archive name.
If the extraction fails (for example, if the files already exist), it skips the extraction.
Args:
archive (str): The path to the archive file.
outdir (str): The directory where the archive should be extracted.
**kwargs: Arbitrary keyword arguments for the patoolib.extract_archive function.
Returns:
None
Raises:
Exception: An exception is raised if the extraction fails for reasons other than the files already existing.
Example:
files = ["sam_hq_vit_tiny.zip", "sam_hq_vit_tiny.z01", "sam_hq_vit_tiny.z02", "sam_hq_vit_tiny.z03"]
base_url = "https://github.com/opengeos/datasets/releases/download/models/"
urls = [base_url + f for f in files]
leafmap.download_files(urls, out_dir="models", multi_part=True)
"""
try:
import patoolib
except ImportError:
install_package("patool")
import patoolib
if not archive.endswith(".zip"):
archive = archive + ".zip"
if outdir is None:
outdir = os.path.dirname(archive)
try:
patoolib.extract_archive(archive, outdir=outdir, **kwargs)
except Exception as e:
print("The unzipped files might already exist. Skipping extraction.")
return
filter_bounds(data, bbox, within=False, align=True, **kwargs)
¶
Filters a GeoDataFrame or GeoSeries by a bounding box.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
data |
str | GeoDataFrame |
The input data to filter. Can be a file path or a GeoDataFrame. |
required |
bbox |
list | GeoDataFrame |
The bounding box to filter by. Can be a list of 4 coordinates or a file path or a GeoDataFrame. |
required |
within |
bool |
Whether to filter by the bounding box or the bounding box's interior. Defaults to False. |
False |
align |
bool |
If True, automatically aligns GeoSeries based on their indices. If False, the order of elements is preserved. |
True |
Returns:
Type | Description |
---|---|
GeoDataFrame |
The filtered data. |
Source code in leafmap/common.py
def filter_bounds(data, bbox, within=False, align=True, **kwargs):
"""Filters a GeoDataFrame or GeoSeries by a bounding box.
Args:
data (str | GeoDataFrame): The input data to filter. Can be a file path or a GeoDataFrame.
bbox (list | GeoDataFrame): The bounding box to filter by. Can be a list of 4 coordinates or a file path or a GeoDataFrame.
within (bool, optional): Whether to filter by the bounding box or the bounding box's interior. Defaults to False.
align (bool, optional): If True, automatically aligns GeoSeries based on their indices. If False, the order of elements is preserved.
Returns:
GeoDataFrame: The filtered data.
"""
import geopandas as gpd
if isinstance(data, str):
data = gpd.read_file(data, **kwargs)
elif not isinstance(data, (gpd.GeoDataFrame, gpd.GeoSeries)):
raise TypeError("data must be a file path or a GeoDataFrame or GeoSeries")
if isinstance(bbox, list):
if len(bbox) != 4:
raise ValueError("bbox must be a list of 4 coordinates")
bbox = bbox_to_gdf(bbox)
elif isinstance(bbox, str):
bbox = gpd.read_file(bbox, **kwargs)
if within:
result = data[data.within(bbox.unary_union, align=align)]
else:
result = data[data.intersects(bbox.unary_union, align=align)]
return result
filter_date(data, start_date=None, end_date=None, date_field='date', date_args={}, **kwargs)
¶
Filters a DataFrame, GeoDataFrame or GeoSeries by a date range.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
data |
str | DataFrame | GeoDataFrame |
The input data to filter. Can be a file path or a DataFrame or GeoDataFrame. |
required |
start_date |
str |
The start date, e.g., 2023-01-01. Defaults to None. |
None |
end_date |
str |
The end date, e.g., 2023-12-31. Defaults to None. |
None |
date_field |
str |
The name of the date field. Defaults to "date". |
'date' |
date_args |
dict |
Additional arguments for pd.to_datetime. Defaults to {}. |
{} |
Returns:
Type | Description |
---|---|
DataFrame |
The filtered data. |
Source code in leafmap/common.py
def filter_date(
data, start_date=None, end_date=None, date_field="date", date_args={}, **kwargs
):
"""Filters a DataFrame, GeoDataFrame or GeoSeries by a date range.
Args:
data (str | DataFrame | GeoDataFrame): The input data to filter. Can be a file path or a DataFrame or GeoDataFrame.
start_date (str, optional): The start date, e.g., 2023-01-01. Defaults to None.
end_date (str, optional): The end date, e.g., 2023-12-31. Defaults to None.
date_field (str, optional): The name of the date field. Defaults to "date".
date_args (dict, optional): Additional arguments for pd.to_datetime. Defaults to {}.
Returns:
DataFrame: The filtered data.
"""
import datetime
import pandas as pd
import geopandas as gpd
if isinstance(data, str):
data = gpd.read_file(data, **kwargs)
elif not isinstance(
data, (gpd.GeoDataFrame, gpd.GeoSeries, pd.DataFrame, pd.Series)
):
raise TypeError("data must be a file path or a GeoDataFrame or GeoSeries")
if date_field not in data.columns:
raise ValueError(f"date_field must be one of {data.columns}")
new_field = f"{date_field}_temp"
data[new_field] = pd.to_datetime(data[date_field], **date_args)
if end_date is None:
end_date = datetime.datetime.now().strftime("%Y-%m-%d")
if start_date is None:
start_date = data[new_field].min()
mask = (data[new_field] >= start_date) & (data[new_field] <= end_date)
result = data.loc[mask]
return result.drop(columns=[new_field], axis=1)
find_files(input_dir, ext=None, fullpath=True, recursive=True)
¶
Find files in a directory.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
input_dir |
str |
The input directory. |
required |
ext |
str |
The file extension to match. Defaults to None. |
None |
fullpath |
bool |
Whether to return the full path. Defaults to True. |
True |
recursive |
bool |
Whether to search recursively. Defaults to True. |
True |
Returns:
Type | Description |
---|---|
list |
A list of matching files. |
Source code in leafmap/common.py
def find_files(input_dir, ext=None, fullpath=True, recursive=True):
"""Find files in a directory.
Args:
input_dir (str): The input directory.
ext (str, optional): The file extension to match. Defaults to None.
fullpath (bool, optional): Whether to return the full path. Defaults to True.
recursive (bool, optional): Whether to search recursively. Defaults to True.
Returns:
list: A list of matching files.
"""
from pathlib import Path
files = []
if ext is None:
ext = "*"
else:
ext = ext.replace(".", "")
ext = f"*.{ext}"
if recursive:
if fullpath:
files = [str(path.joinpath()) for path in Path(input_dir).rglob(ext)]
else:
files = [str(path.name) for path in Path(input_dir).rglob(ext)]
else:
if fullpath:
files = [str(path.joinpath()) for path in Path(input_dir).glob(ext)]
else:
files = [path.name for path in Path(input_dir).glob(ext)]
files.sort()
return files
gdb_layer_names(gdb_path)
¶
Get a list of layer names in a File Geodatabase (GDB).
Parameters:
Name | Type | Description | Default |
---|---|---|---|
gdb_path |
str |
The path to the File Geodatabase (GDB). |
required |
Returns:
Type | Description |
---|---|
List[str] |
A list of layer names in the GDB. |
Source code in leafmap/common.py
def gdb_layer_names(gdb_path: str) -> List[str]:
"""Get a list of layer names in a File Geodatabase (GDB).
Args:
gdb_path (str): The path to the File Geodatabase (GDB).
Returns:
List[str]: A list of layer names in the GDB.
"""
from osgeo import ogr
# Open the GDB
gdb_driver = ogr.GetDriverByName("OpenFileGDB")
gdb_dataset = gdb_driver.Open(gdb_path, 0)
# Get the number of layers in the GDB
layer_count = gdb_dataset.GetLayerCount()
# Iterate over the layers
layer_names = []
for i in range(layer_count):
layer = gdb_dataset.GetLayerByIndex(i)
feature_class_name = layer.GetName()
layer_names.append(feature_class_name)
# Close the GDB dataset
gdb_dataset = None
return layer_names
gdb_to_vector(gdb_path, out_dir, layers=None, filenames=None, gdal_driver='GPKG', file_extension=None, overwrite=False, quiet=False, **kwargs)
¶
Converts layers from a File Geodatabase (GDB) to a vector format.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
gdb_path |
str |
The path to the File Geodatabase (GDB). |
required |
out_dir |
str |
The output directory to save the converted files. |
required |
layers |
Optional[List[str]] |
A list of layer names to convert. If None, all layers will be converted. Default is None. |
None |
filenames |
Optional[List[str]] |
A list of output file names. If None, the layer names will be used as the file names. Default is None. |
None |
gdal_driver |
str |
The GDAL driver name for the output vector format. Default is "GPKG". |
'GPKG' |
file_extension |
Optional[str] |
The file extension for the output files. If None, it will be determined automatically based on the gdal_driver. Default is None. |
None |
overwrite |
bool |
Whether to overwrite the existing output files. Default is False. |
False |
quiet |
bool |
If True, suppress the log output. Defaults to False. |
False |
Returns:
Type | Description |
---|---|
None |
None |
Source code in leafmap/common.py
def gdb_to_vector(
gdb_path: str,
out_dir: str,
layers: Optional[List[str]] = None,
filenames: Optional[List[str]] = None,
gdal_driver: str = "GPKG",
file_extension: Optional[str] = None,
overwrite: bool = False,
quiet=False,
**kwargs,
) -> None:
"""Converts layers from a File Geodatabase (GDB) to a vector format.
Args:
gdb_path (str): The path to the File Geodatabase (GDB).
out_dir (str): The output directory to save the converted files.
layers (Optional[List[str]]): A list of layer names to convert. If None, all layers will be converted. Default is None.
filenames (Optional[List[str]]): A list of output file names. If None, the layer names will be used as the file names. Default is None.
gdal_driver (str): The GDAL driver name for the output vector format. Default is "GPKG".
file_extension (Optional[str]): The file extension for the output files. If None, it will be determined automatically based on the gdal_driver. Default is None.
overwrite (bool): Whether to overwrite the existing output files. Default is False.
quiet (bool): If True, suppress the log output. Defaults to False.
Returns:
None
"""
from osgeo import ogr
# Open the GDB
gdb_driver = ogr.GetDriverByName("OpenFileGDB")
gdb_dataset = gdb_driver.Open(gdb_path, 0)
# Get the number of layers in the GDB
layer_count = gdb_dataset.GetLayerCount()
if isinstance(layers, str):
layers = [layers]
if isinstance(filenames, str):
filenames = [filenames]
if filenames is not None:
if len(filenames) != len(layers):
raise ValueError("The length of filenames must match the length of layers.")
if not os.path.exists(out_dir):
os.makedirs(out_dir)
ii = 0
# Iterate over the layers
for i in range(layer_count):
layer = gdb_dataset.GetLayerByIndex(i)
feature_class_name = layer.GetName()
if layers is not None:
if feature_class_name not in layers:
continue
if file_extension is None:
file_extension = get_gdal_file_extension(gdal_driver)
# Create the output file path
if filenames is not None:
output_file = os.path.join(out_dir, filenames[ii] + "." + file_extension)
ii += 1
else:
output_file = os.path.join(
out_dir, feature_class_name + "." + file_extension
)
if os.path.exists(output_file) and not overwrite:
print(f"File {output_file} already exists. Skipping...")
continue
else:
if not quiet:
print(f"Converting layer {feature_class_name} to {output_file}...")
# Create the output driver
output_driver = ogr.GetDriverByName(gdal_driver)
output_dataset = output_driver.CreateDataSource(output_file)
# Copy the input layer to the output format
output_dataset.CopyLayer(layer, feature_class_name)
output_dataset = None
# Close the GDB dataset
gdb_dataset = None
gdf_bounds(gdf, return_geom=False)
¶
Returns the bounding box of a GeoDataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
gdf |
gpd.GeoDataFrame |
A GeoDataFrame. |
required |
return_geom |
bool |
Whether to return the bounding box as a GeoDataFrame. Defaults to False. |
False |
Returns:
Type | Description |
---|---|
list | gpd.GeoDataFrame |
A bounding box in the form of a list (minx, miny, maxx, maxy) or GeoDataFrame. |
Source code in leafmap/common.py
def gdf_bounds(gdf, return_geom=False):
"""Returns the bounding box of a GeoDataFrame.
Args:
gdf (gpd.GeoDataFrame): A GeoDataFrame.
return_geom (bool, optional): Whether to return the bounding box as a GeoDataFrame. Defaults to False.
Returns:
list | gpd.GeoDataFrame: A bounding box in the form of a list (minx, miny, maxx, maxy) or GeoDataFrame.
"""
bounds = gdf.total_bounds
if return_geom:
return bbox_to_gdf(bbox=bounds)
else:
return bounds
gdf_centroid(gdf, return_geom=False)
¶
Returns the centroid of a GeoDataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
gdf |
gpd.GeoDataFrame |
A GeoDataFrame. |
required |
return_geom |
bool |
Whether to return the bounding box as a GeoDataFrame. Defaults to False. |
False |
Returns:
Type | Description |
---|---|
list | gpd.GeoDataFrame |
A bounding box in the form of a list (lon, lat) or GeoDataFrame. |
Source code in leafmap/common.py
def gdf_centroid(gdf, return_geom=False):
"""Returns the centroid of a GeoDataFrame.
Args:
gdf (gpd.GeoDataFrame): A GeoDataFrame.
return_geom (bool, optional): Whether to return the bounding box as a GeoDataFrame. Defaults to False.
Returns:
list | gpd.GeoDataFrame: A bounding box in the form of a list (lon, lat) or GeoDataFrame.
"""
warnings.filterwarnings("ignore")
centroid = gdf_bounds(gdf, return_geom=True).centroid
if return_geom:
return centroid
else:
return centroid.x[0], centroid.y[0]
gdf_geom_type(gdf, first_only=True)
¶
Returns the geometry type of a GeoDataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
gdf |
gpd.GeoDataFrame |
A GeoDataFrame. |
required |
first_only |
bool |
Whether to return the geometry type of the f irst feature in the GeoDataFrame. Defaults to True. |
True |
Returns:
Type | Description |
---|---|
str |
The geometry type of the GeoDataFrame, such as Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon. For more info, see https://shapely.readthedocs.io/en/stable/manual.html |
Source code in leafmap/common.py
def gdf_geom_type(gdf, first_only=True):
"""Returns the geometry type of a GeoDataFrame.
Args:
gdf (gpd.GeoDataFrame): A GeoDataFrame.
first_only (bool, optional): Whether to return the geometry type of the f
irst feature in the GeoDataFrame. Defaults to True.
Returns:
str: The geometry type of the GeoDataFrame, such as Point, LineString,
Polygon, MultiPoint, MultiLineString, MultiPolygon.
For more info, see https://shapely.readthedocs.io/en/stable/manual.html
"""
import geopandas as gpd
if first_only:
return gdf.geometry.type[0]
else:
return gdf.geometry.type
gdf_to_bokeh(gdf)
¶
Function to convert a GeoPandas GeoDataFrame to a Bokeh ColumnDataSource object.
:param: (GeoDataFrame) gdf: GeoPandas GeoDataFrame with polygon(s) under the column name 'geometry.'
:return: ColumnDataSource for Bokeh.
Source code in leafmap/common.py
def gdf_to_bokeh(gdf):
"""
Function to convert a GeoPandas GeoDataFrame to a Bokeh
ColumnDataSource object.
:param: (GeoDataFrame) gdf: GeoPandas GeoDataFrame with polygon(s) under
the column name 'geometry.'
:return: ColumnDataSource for Bokeh.
"""
from bokeh.plotting import ColumnDataSource
shape_type = gdf_geom_type(gdf)
gdf_new = gdf.drop("geometry", axis=1).copy()
gdf_new["x"] = gdf.apply(
get_geometry_coords,
geom="geometry",
coord_type="x",
shape_type=shape_type,
mercator=True,
axis=1,
)
gdf_new["y"] = gdf.apply(
get_geometry_coords,
geom="geometry",
coord_type="y",
shape_type=shape_type,
mercator=True,
axis=1,
)
return ColumnDataSource(gdf_new)
gdf_to_df(gdf, drop_geom=True)
¶
Converts a GeoDataFrame to a pandas DataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
gdf |
gpd.GeoDataFrame |
A GeoDataFrame. |
required |
drop_geom |
bool |
Whether to drop the geometry column. Defaults to True. |
True |
Returns:
Type | Description |
---|---|
pd.DataFrame |
A pandas DataFrame containing the GeoDataFrame. |
Source code in leafmap/common.py
def gdf_to_df(gdf, drop_geom=True):
"""Converts a GeoDataFrame to a pandas DataFrame.
Args:
gdf (gpd.GeoDataFrame): A GeoDataFrame.
drop_geom (bool, optional): Whether to drop the geometry column. Defaults to True.
Returns:
pd.DataFrame: A pandas DataFrame containing the GeoDataFrame.
"""
import pandas as pd
if drop_geom:
df = pd.DataFrame(gdf.drop(columns=["geometry"]))
else:
df = pd.DataFrame(gdf)
return df
gdf_to_geojson(gdf, out_geojson=None, epsg=None, tuple_to_list=False, encoding='utf-8')
¶
Converts a GeoDataFame to GeoJSON.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
gdf |
GeoDataFrame |
A GeoPandas GeoDataFrame. |
required |
out_geojson |
str |
File path to he output GeoJSON. Defaults to None. |
None |
epsg |
str |
An EPSG string, e.g., "4326". Defaults to None. |
None |
tuple_to_list |
bool |
Whether to convert tuples to lists. Defaults to False. |
False |
encoding |
str |
The encoding to use for the GeoJSON. Defaults to "utf-8". |
'utf-8' |
Exceptions:
Type | Description |
---|---|
TypeError |
When the output file extension is incorrect. |
Exception |
When the conversion fails. |
Returns:
Type | Description |
---|---|
dict |
When the out_json is None returns a dict. |
Source code in leafmap/common.py
def gdf_to_geojson(
gdf, out_geojson=None, epsg=None, tuple_to_list=False, encoding="utf-8"
):
"""Converts a GeoDataFame to GeoJSON.
Args:
gdf (GeoDataFrame): A GeoPandas GeoDataFrame.
out_geojson (str, optional): File path to he output GeoJSON. Defaults to None.
epsg (str, optional): An EPSG string, e.g., "4326". Defaults to None.
tuple_to_list (bool, optional): Whether to convert tuples to lists. Defaults to False.
encoding (str, optional): The encoding to use for the GeoJSON. Defaults to "utf-8".
Raises:
TypeError: When the output file extension is incorrect.
Exception: When the conversion fails.
Returns:
dict: When the out_json is None returns a dict.
"""
check_package(name="geopandas", URL="https://geopandas.org")
def listit(t):
return list(map(listit, t)) if isinstance(t, (list, tuple)) else t
try:
if epsg is not None:
if gdf.crs is not None and gdf.crs.to_epsg() != epsg:
gdf = gdf.to_crs(epsg=epsg)
geojson = gdf.__geo_interface__
if tuple_to_list:
for feature in geojson["features"]:
feature["geometry"]["coordinates"] = listit(
feature["geometry"]["coordinates"]
)
if out_geojson is None:
return geojson
else:
ext = os.path.splitext(out_geojson)[1]
if ext.lower() not in [".json", ".geojson"]:
raise TypeError(
"The output file extension must be either .json or .geojson"
)
out_dir = os.path.dirname(out_geojson)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
gdf.to_file(out_geojson, driver="GeoJSON", encoding=encoding)
except Exception as e:
raise Exception(e)
gedi_download_file(url, filename=None, username=None, password=None)
¶
Downloads a file from the given URL and saves it to the specified filename. If no filename is provided, the name of the file from the URL will be used.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
The URL of the file to download. e.g., https://daac.ornl.gov/daacdata/gedi/GEDI_L4A_AGB_Density_V2_1/data/GEDI04_A_2019298202754_O04921_01_T02899_02_002_02_V002.h5 |
required |
filename |
str |
The name of the file to save the downloaded content to. Defaults to None. |
None |
username |
str |
Username for authentication. Can also be set using the EARTHDATA_USERNAME environment variable. Defaults to None. Create an account at https://urs.earthdata.nasa.gov |
None |
password |
str |
Password for authentication. Can also be set using the EARTHDATA_PASSWORD environment variable. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
None |
None |
Source code in leafmap/common.py
def gedi_download_file(
url: str, filename: str = None, username: str = None, password: str = None
) -> None:
"""
Downloads a file from the given URL and saves it to the specified filename.
If no filename is provided, the name of the file from the URL will be used.
Args:
url (str): The URL of the file to download.
e.g., https://daac.ornl.gov/daacdata/gedi/GEDI_L4A_AGB_Density_V2_1/data/GEDI04_A_2019298202754_O04921_01_T02899_02_002_02_V002.h5
filename (str, optional): The name of the file to save the downloaded content to. Defaults to None.
username (str, optional): Username for authentication. Can also be set using the EARTHDATA_USERNAME environment variable. Defaults to None.
Create an account at https://urs.earthdata.nasa.gov
password (str, optional): Password for authentication. Can also be set using the EARTHDATA_PASSWORD environment variable. Defaults to None.
Returns:
None
"""
import requests
from tqdm import tqdm
from urllib.parse import urlparse
if username is None:
username = os.environ.get("EARTHDATA_USERNAME", None)
if password is None:
password = os.environ.get("EARTHDATA_PASSWORD", None)
if username is None or password is None:
raise ValueError(
"Username and password must be provided. Create an account at https://urs.earthdata.nasa.gov."
)
with requests.Session() as session:
r1 = session.request("get", url, stream=True)
r = session.get(r1.url, auth=(username, password), stream=True)
print(r.status_code)
if r.status_code == 200:
total_size = int(r.headers.get("content-length", 0))
block_size = 1024 # 1 KB
# Use the filename from the URL if not provided
if not filename:
parsed_url = urlparse(url)
filename = parsed_url.path.split("/")[-1]
progress_bar = tqdm(total=total_size, unit="B", unit_scale=True)
with open(filename, "wb") as file:
for data in r.iter_content(block_size):
progress_bar.update(len(data))
file.write(data)
progress_bar.close()
gedi_download_files(urls, outdir=None, filenames=None, username=None, password=None, overwrite=False)
¶
Downloads files from the given URLs and saves them to the specified directory. If no directory is provided, the current directory will be used. If no filenames are provided, the names of the files from the URLs will be used.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
urls |
List[str] |
The URLs of the files to download. e.g., ["https://example.com/file1.txt", "https://example.com/file2.txt"] |
required |
outdir |
str |
The directory to save the downloaded files to. Defaults to None. |
None |
filenames |
str |
The names of the files to save the downloaded content to. Defaults to None. |
None |
username |
str |
Username for authentication. Can also be set using the EARTHDATA_USERNAME environment variable. Defaults to None. Create an account at https://urs.earthdata.nasa.gov |
None |
password |
str |
Password for authentication. Can also be set using the EARTHDATA_PASSWORD environment variable. Defaults to None. |
None |
overwrite |
bool |
Whether to overwrite the existing output files. Default is False. |
False |
Returns:
Type | Description |
---|---|
None |
None |
Source code in leafmap/common.py
def gedi_download_files(
urls: List[str],
outdir: str = None,
filenames: str = None,
username: str = None,
password: str = None,
overwrite: bool = False,
) -> None:
"""
Downloads files from the given URLs and saves them to the specified directory.
If no directory is provided, the current directory will be used.
If no filenames are provided, the names of the files from the URLs will be used.
Args:
urls (List[str]): The URLs of the files to download.
e.g., ["https://example.com/file1.txt", "https://example.com/file2.txt"]
outdir (str, optional): The directory to save the downloaded files to. Defaults to None.
filenames (str, optional): The names of the files to save the downloaded content to. Defaults to None.
username (str, optional): Username for authentication. Can also be set using the EARTHDATA_USERNAME environment variable. Defaults to None.
Create an account at https://urs.earthdata.nasa.gov
password (str, optional): Password for authentication. Can also be set using the EARTHDATA_PASSWORD environment variable. Defaults to None.
overwrite (bool): Whether to overwrite the existing output files. Default is False.
Returns:
None
"""
import requests
from tqdm import tqdm
from urllib.parse import urlparse
import geopandas as gpd
if isinstance(urls, gpd.GeoDataFrame):
urls = urls["granule_url"].tolist()
session = requests.Session()
if username is None:
username = os.environ.get("EARTHDATA_USERNAME", None)
if password is None:
password = os.environ.get("EARTHDATA_PASSWORD", None)
if username is None or password is None:
print("Username and password must be provided.")
return
if outdir is None:
outdir = os.getcwd()
if not os.path.exists(outdir):
os.makedirs(outdir)
for index, url in enumerate(urls):
print(f"Downloading file {index+1} of {len(urls)}...")
if url is None:
continue
# Use the filename from the URL if not provided
if not filenames:
parsed_url = urlparse(url)
filename = parsed_url.path.split("/")[-1]
else:
filename = filenames.pop(0)
filepath = os.path.join(outdir, filename)
if os.path.exists(filepath) and not overwrite:
print(f"File {filepath} already exists. Skipping...")
continue
r1 = session.request("get", url, stream=True)
r = session.get(r1.url, auth=(username, password), stream=True)
if r.status_code == 200:
total_size = int(r.headers.get("content-length", 0))
block_size = 1024 # 1 KB
progress_bar = tqdm(total=total_size, unit="B", unit_scale=True)
with open(filepath, "wb") as file:
for data in r.iter_content(block_size):
progress_bar.update(len(data))
file.write(data)
progress_bar.close()
session.close()
gedi_search(roi, start_date=None, end_date=None, add_roi=False, return_type='gdf', output=None, sort_filesize=False, **kwargs)
¶
Searches for GEDI data using the Common Metadata Repository (CMR) API. The source code for this function is adapted from https://github.com/ornldaac/gedi_tutorials. Credits to ORNL DAAC and Rupesh Shrestha.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
roi |
A list, tuple, or file path representing the bounding box coordinates in the format (min_lon, min_lat, max_lon, max_lat), or a GeoDataFrame containing the region of interest geometry. |
required | |
start_date |
Optional[str] |
The start date of the temporal range to search for data in the format 'YYYY-MM-DD'. |
None |
end_date |
Optional[str] |
The end date of the temporal range to search for data in the format 'YYYY-MM-DD'. |
None |
add_roi |
bool |
A boolean value indicating whether to include the region of interest as a granule in the search results. Default is False. |
False |
return_type |
str |
The type of the search results to return. Must be one of 'df' (DataFrame), 'gdf' (GeoDataFrame), or 'csv' (CSV file). Default is 'gdf'. |
'gdf' |
output |
Optional[str] |
The file path to save the CSV output when return_type is 'csv'. Optional and only applicable when return_type is 'csv'. |
None |
sort_filesize |
bool |
A boolean value indicating whether to sort the search results. |
False |
**kwargs |
Additional keyword arguments to be passed to the CMR API. |
{} |
Returns:
Type | Description |
---|---|
Optional[pandas.core.frame.DataFrame] |
The search results as a pandas DataFrame (return_type='df'), geopandas GeoDataFrame (return_type='gdf'), or a CSV file (return_type='csv'). |
Exceptions:
Type | Description |
---|---|
ValueError |
If roi is not a list, tuple, or file path. |
Source code in leafmap/common.py
def gedi_search(
roi,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
add_roi: bool = False,
return_type: str = "gdf",
output: Optional[str] = None,
sort_filesize: bool = False,
**kwargs,
) -> Union[pd.DataFrame, None]:
"""
Searches for GEDI data using the Common Metadata Repository (CMR) API.
The source code for this function is adapted from https://github.com/ornldaac/gedi_tutorials.
Credits to ORNL DAAC and Rupesh Shrestha.
Args:
roi: A list, tuple, or file path representing the bounding box coordinates
in the format (min_lon, min_lat, max_lon, max_lat), or a GeoDataFrame
containing the region of interest geometry.
start_date: The start date of the temporal range to search for data
in the format 'YYYY-MM-DD'.
end_date: The end date of the temporal range to search for data
in the format 'YYYY-MM-DD'.
add_roi: A boolean value indicating whether to include the region of interest
as a granule in the search results. Default is False.
return_type: The type of the search results to return. Must be one of 'df'
(DataFrame), 'gdf' (GeoDataFrame), or 'csv' (CSV file). Default is 'gdf'.
output: The file path to save the CSV output when return_type is 'csv'.
Optional and only applicable when return_type is 'csv'.
sort_filesize: A boolean value indicating whether to sort the search results.
**kwargs: Additional keyword arguments to be passed to the CMR API.
Returns:
The search results as a pandas DataFrame (return_type='df'), geopandas GeoDataFrame
(return_type='gdf'), or a CSV file (return_type='csv').
Raises:
ValueError: If roi is not a list, tuple, or file path.
"""
import requests
import datetime as dt
import pandas as pd
import geopandas as gpd
from shapely.geometry import MultiPolygon, Polygon, box
from shapely.ops import orient
# CMR API base url
cmrurl = "https://cmr.earthdata.nasa.gov/search/"
doi = "10.3334/ORNLDAAC/2056" # GEDI L4A DOI
# Construct the DOI search URL
doisearch = cmrurl + "collections.json?doi=" + doi
# Send a request to the CMR API to get the concept ID
response = requests.get(doisearch)
response.raise_for_status()
concept_id = response.json()["feed"]["entry"][0]["id"]
# CMR formatted start and end times
if start_date is not None and end_date is not None:
dt_format = "%Y-%m-%dT%H:%M:%SZ"
start_date = dt.datetime.strptime(start_date, "%Y-%m-%d")
end_date = dt.datetime.strptime(end_date, "%Y-%m-%d")
temporal_str = (
start_date.strftime(dt_format) + "," + end_date.strftime(dt_format)
)
else:
temporal_str = None
# CMR formatted bounding box
if isinstance(roi, list) or isinstance(roi, tuple):
bound_str = ",".join(map(str, roi))
elif isinstance(roi, str):
roi = gpd.read_file(roi)
roi.geometry = roi.geometry.apply(orient, args=(1,)) # make counter-clockwise
elif isinstance(roi, gpd.GeoDataFrame):
roi.geometry = roi.geometry.apply(orient, args=(1,)) # make counter-clockwise
else:
raise ValueError("roi must be a list, tuple, or a file path.")
page_num = 1
page_size = 2000 # CMR page size limit
granule_arr = []
while True:
# Define CMR search parameters
cmr_param = {
"collection_concept_id": concept_id,
"page_size": page_size,
"page_num": page_num,
}
if temporal_str is not None:
cmr_param["temporal"] = temporal_str
if kwargs:
cmr_param.update(kwargs)
granulesearch = cmrurl + "granules.json"
if isinstance(roi, list) or isinstance(roi, tuple):
cmr_param["bounding_box[]"] = bound_str
response = requests.get(granulesearch, params=cmr_param)
response.raise_for_status()
else:
cmr_param["simplify-shapefile"] = "true"
geojson = {
"shapefile": (
"region.geojson",
roi.geometry.to_json(),
"application/geo+json",
)
}
response = requests.post(granulesearch, data=cmr_param, files=geojson)
# Send a request to the CMR API to get the granules
granules = response.json()["feed"]["entry"]
if granules:
for index, g in enumerate(granules):
granule_url = ""
granule_poly = ""
# Read file size
granule_size = float(g["granule_size"])
# Read bounding geometries
if "polygons" in g:
polygons = g["polygons"]
multipolygons = []
for poly in polygons:
i = iter(poly[0].split(" "))
ltln = list(map(" ".join, zip(i, i)))
multipolygons.append(
Polygon(
[
[float(p.split(" ")[1]), float(p.split(" ")[0])]
for p in ltln
]
)
)
granule_poly = MultiPolygon(multipolygons)
# Get URL to HDF5 files
for links in g["links"]:
if (
"title" in links
and links["title"].startswith("Download")
and links["title"].endswith(".h5")
):
granule_url = links["href"]
granule_id = g["id"]
title = g["title"]
time_start = g["time_start"]
time_end = g["time_end"]
granule_arr.append(
[
granule_id,
title,
time_start,
time_end,
granule_size,
granule_url,
granule_poly,
]
)
page_num += 1
else:
break
# Add bound as the last row into the dataframe
if add_roi:
if isinstance(roi, list) or isinstance(roi, tuple):
b = list(roi)
granule_arr.append(
["roi", None, None, None, 0, None, box(b[0], b[1], b[2], b[3])]
)
else:
granule_arr.append(["roi", None, None, None, 0, None, roi.geometry.item()])
# Create a pandas dataframe
columns = [
"id",
"title",
"time_start",
"time_end",
"granule_size",
"granule_url",
"granule_poly",
]
l4adf = pd.DataFrame(granule_arr, columns=columns)
# Drop granules with empty geometry
l4adf = l4adf[l4adf["granule_poly"] != ""]
if sort_filesize:
l4adf = l4adf.sort_values(by=["granule_size"], ascending=True)
if return_type == "df":
return l4adf
elif return_type == "gdf":
gdf = gpd.GeoDataFrame(l4adf, geometry="granule_poly")
gdf.crs = "EPSG:4326"
return gdf
elif return_type == "csv":
columns.remove("granule_poly")
return l4adf.to_csv(output, index=False, columns=columns)
else:
raise ValueError("return_type must be one of 'df', 'gdf', or 'csv'.")
gedi_subset(spatial=None, start_date=None, end_date=None, out_dir=None, collection=None, variables=['all'], max_results=None, username=None, password=None, overwrite=False, **kwargs)
¶
Subsets GEDI data using the Harmony API.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
spatial |
Union[str, gpd.GeoDataFrame, List[float]] |
Spatial extent for subsetting. Can be a file path to a shapefile, a GeoDataFrame, or a list of bounding box coordinates [minx, miny, maxx, maxy]. Defaults to None. |
None |
start_date |
str |
Start date for subsetting in 'YYYY-MM-DD' format. Defaults to None. |
None |
end_date |
str |
End date for subsetting in 'YYYY-MM-DD' format. Defaults to None. |
None |
out_dir |
str |
Output directory to save the subsetted files. Defaults to None, which will use the current working directory. |
None |
collection |
Collection |
GEDI data collection. If not provided, the default collection with DOI '10.3334/ORNLDAAC/2056' will be used. Defaults to None. |
None |
variables |
List[str] |
List of variable names to subset. Defaults to ['all'], which subsets all available variables. |
['all'] |
max_results |
int |
Maximum number of results to return. Defaults to None, which returns all results. |
None |
username |
str |
Earthdata username. Defaults to None, which will attempt to read from the 'EARTHDATA_USERNAME' environment variable. |
None |
password |
str |
Earthdata password. Defaults to None, which will attempt to read from the 'EARTHDATA_PASSWORD' environment variable. |
None |
overwrite |
bool |
Whether to overwrite existing files in the output directory. Defaults to False. |
False |
**kwargs |
Additional keyword arguments to pass to the Harmony API request. |
{} |
Exceptions:
Type | Description |
---|---|
ImportError |
If the 'harmony' package is not installed. |
ValueError |
If the 'spatial', 'start_date', or 'end_date' arguments are not valid. |
Returns:
Type | Description |
---|---|
None |
This function does not return any value. |
Source code in leafmap/common.py
def gedi_subset(
spatial=None,
start_date=None,
end_date=None,
out_dir=None,
collection=None,
variables=["all"],
max_results=None,
username=None,
password=None,
overwrite=False,
**kwargs,
):
"""
Subsets GEDI data using the Harmony API.
Args:
spatial (Union[str, gpd.GeoDataFrame, List[float]], optional): Spatial extent for subsetting.
Can be a file path to a shapefile, a GeoDataFrame, or a list of bounding box coordinates [minx, miny, maxx, maxy].
Defaults to None.
start_date (str, optional): Start date for subsetting in 'YYYY-MM-DD' format.
Defaults to None.
end_date (str, optional): End date for subsetting in 'YYYY-MM-DD' format.
Defaults to None.
out_dir (str, optional): Output directory to save the subsetted files.
Defaults to None, which will use the current working directory.
collection (Collection, optional): GEDI data collection. If not provided,
the default collection with DOI '10.3334/ORNLDAAC/2056' will be used.
Defaults to None.
variables (List[str], optional): List of variable names to subset.
Defaults to ['all'], which subsets all available variables.
max_results (int, optional): Maximum number of results to return.
Defaults to None, which returns all results.
username (str, optional): Earthdata username.
Defaults to None, which will attempt to read from the 'EARTHDATA_USERNAME' environment variable.
password (str, optional): Earthdata password.
Defaults to None, which will attempt to read from the 'EARTHDATA_PASSWORD' environment variable.
overwrite (bool, optional): Whether to overwrite existing files in the output directory.
Defaults to False.
**kwargs: Additional keyword arguments to pass to the Harmony API request.
Raises:
ImportError: If the 'harmony' package is not installed.
ValueError: If the 'spatial', 'start_date', or 'end_date' arguments are not valid.
Returns:
None: This function does not return any value.
"""
try:
import harmony # pylint: disable=E0401
except ImportError:
install_package("harmony-py")
import requests as re
import geopandas as gpd
from datetime import datetime
from harmony import (
BBox,
Client,
Collection,
Environment,
Request,
) # pylint: disable=E0401
if out_dir is None:
out_dir = os.getcwd()
if not os.path.exists(out_dir):
os.makedirs(out_dir)
if collection is None:
# GEDI L4A DOI
doi = "10.3334/ORNLDAAC/2056"
# CMR API base url
doisearch = f"https://cmr.earthdata.nasa.gov/search/collections.json?doi={doi}"
concept_id = re.get(doisearch).json()["feed"]["entry"][0]["id"]
concept_id
collection = Collection(id=concept_id)
if username is None:
username = os.environ.get("EARTHDATA_USERNAME", None)
if password is None:
password = os.environ.get("EARTHDATA_PASSWORD", None)
if username is None or password is None:
raise ValueError("username and password must be provided.")
harmony_client = Client(auth=(username, password))
if isinstance(spatial, str):
spatial = gpd.read_file(spatial)
if isinstance(spatial, gpd.GeoDataFrame):
spatial = spatial.total_bounds.tolist()
if isinstance(spatial, list) and len(spatial) == 4:
bounding_box = BBox(spatial[0], spatial[1], spatial[2], spatial[3])
else:
raise ValueError(
"spatial must be a list of bounding box coordinates or a GeoDataFrame, or a file path."
)
if isinstance(start_date, str):
start_date = datetime.strptime(start_date, "%Y-%m-%d")
if isinstance(end_date, str):
end_date = datetime.strptime(end_date, "%Y-%m-%d")
if start_date is None or end_date is None:
print("start_date and end_date must be provided.")
temporal_range = None
else:
temporal_range = {"start": start_date, "end": end_date}
request = Request(
collection=collection,
variables=variables,
temporal=temporal_range,
spatial=bounding_box,
ignore_errors=True,
max_results=max_results,
**kwargs,
)
# submit harmony request, will return job id
subset_job_id = harmony_client.submit(request)
print(f"Processing job: {subset_job_id}")
print(f"Waiting for the job to finish")
results = harmony_client.result_json(subset_job_id, show_progress=True)
print(f"Downloading subset files...")
futures = harmony_client.download_all(
subset_job_id, directory=out_dir, overwrite=overwrite
)
for f in futures:
# all subsetted files have this suffix
if f.result().endswith("subsetted.h5"):
print(f"Downloaded: {f.result()}")
print(f"Done downloading files.")
generate_index_html(directory, output='index.html')
¶
Generates an HTML file named 'index.html' in the specified directory, listing all files in that directory as clickable links.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
directory |
str |
The path to the directory for which to generate the index.html file. |
required |
output |
str |
The name of the output HTML file. Defaults to "index.html". |
'index.html' |
Returns:
Type | Description |
---|---|
None |
None |
Source code in leafmap/common.py
def generate_index_html(directory: str, output: str = "index.html") -> None:
"""
Generates an HTML file named 'index.html' in the specified directory, listing
all files in that directory as clickable links.
Args:
directory (str): The path to the directory for which to generate the index.html file.
output (str, optional): The name of the output HTML file. Defaults to "index.html".
Returns:
None
"""
# Get a list of files in the directory
files = sorted(
[f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))]
)
# Start the HTML content
html_content = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Index of {directory}</title>
</head>
<body>
<h1>Index of {directory}</h1>
<ul>
""".format(
directory=directory
)
# Add each file to the HTML list
for file in files:
html_content += ' <li><a href="{file}">{file}</a></li>\n'.format(
file=file
)
# Close the HTML content
html_content += """ </ul>
</body>
</html>"""
# Write the HTML content to index.html in the specified directory
with open(os.path.join(directory, output), "w") as f:
f.write(html_content)
geojson_bounds(geojson)
¶
Calculate the bounds of a GeoJSON object.
This function uses the shapely library to calculate the bounds of a GeoJSON object. If the shapely library is not installed, it will print a message and return None.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
geojson |
dict |
A dictionary representing a GeoJSON object. |
required |
Returns:
Type | Description |
---|---|
list |
A list of bounds (minx, miny, maxx, maxy) if shapely is installed, None otherwise. |
Source code in leafmap/common.py
def geojson_bounds(geojson: dict) -> Optional[list]:
"""
Calculate the bounds of a GeoJSON object.
This function uses the shapely library to calculate the bounds of a GeoJSON object.
If the shapely library is not installed, it will print a message and return None.
Args:
geojson (dict): A dictionary representing a GeoJSON object.
Returns:
list: A list of bounds (minx, miny, maxx, maxy) if shapely is installed, None otherwise.
"""
try:
import shapely
except ImportError:
print("shapely is not installed")
return
if isinstance(geojson, str):
geojson = json.loads(geojson)
return list(shapely.bounds(shapely.from_geojson(json.dumps(geojson))))
geojson_to_df(in_geojson, encoding='utf-8', drop_geometry=True)
¶
Converts a GeoJSON object to a pandas DataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_geojson |
str | dict |
The input GeoJSON file or dict. |
required |
encoding |
str |
The encoding of the GeoJSON object. Defaults to "utf-8". |
'utf-8' |
drop_geometry |
bool |
Whether to drop the geometry column. Defaults to True. |
True |
Exceptions:
Type | Description |
---|---|
FileNotFoundError |
If the input GeoJSON file could not be found. |
Returns:
Type | Description |
---|---|
pd.DataFrame |
A pandas DataFrame containing the GeoJSON object. |
Source code in leafmap/common.py
def geojson_to_df(in_geojson, encoding="utf-8", drop_geometry=True):
"""Converts a GeoJSON object to a pandas DataFrame.
Args:
in_geojson (str | dict): The input GeoJSON file or dict.
encoding (str, optional): The encoding of the GeoJSON object. Defaults to "utf-8".
drop_geometry (bool, optional): Whether to drop the geometry column. Defaults to True.
Raises:
FileNotFoundError: If the input GeoJSON file could not be found.
Returns:
pd.DataFrame: A pandas DataFrame containing the GeoJSON object.
"""
import json
import pandas as pd
from urllib.request import urlopen
if isinstance(in_geojson, str):
if in_geojson.startswith("http"):
with urlopen(in_geojson) as f:
data = json.load(f)
else:
in_geojson = os.path.abspath(in_geojson)
if not os.path.exists(in_geojson):
raise FileNotFoundError("The provided GeoJSON file could not be found.")
with open(in_geojson, encoding=encoding) as f:
data = json.load(f)
elif isinstance(in_geojson, dict):
data = in_geojson
df = pd.json_normalize(data["features"])
df.columns = [col.replace("properties.", "") for col in df.columns]
if drop_geometry:
df = df[df.columns.drop(list(df.filter(regex="geometry")))]
return df
geojson_to_gdf(in_geojson, encoding='utf-8', **kwargs)
¶
Converts a GeoJSON object to a geopandas GeoDataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_geojson |
str | dict |
The input GeoJSON file or GeoJSON object as a dict. |
required |
encoding |
str |
The encoding of the GeoJSON object. Defaults to "utf-8". |
'utf-8' |
Returns:
Type | Description |
---|---|
geopandas.GeoDataFrame |
A geopandas GeoDataFrame containing the GeoJSON object. |
Source code in leafmap/common.py
def geojson_to_gdf(in_geojson, encoding="utf-8", **kwargs):
"""Converts a GeoJSON object to a geopandas GeoDataFrame.
Args:
in_geojson (str | dict): The input GeoJSON file or GeoJSON object as a dict.
encoding (str, optional): The encoding of the GeoJSON object. Defaults to "utf-8".
Returns:
geopandas.GeoDataFrame: A geopandas GeoDataFrame containing the GeoJSON object.
"""
import geopandas as gpd
if isinstance(in_geojson, dict):
out_file = temp_file_path(extension="geojson")
with open(out_file, "w") as f:
json.dump(in_geojson, f)
in_geojson = out_file
gdf = gpd.read_file(in_geojson, encoding=encoding, **kwargs)
return gdf
geojson_to_gpkg(in_geojson, out_gpkg, **kwargs)
¶
Converts a GeoJSON object to GeoPackage.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_geojson |
str | dict |
The input GeoJSON file or dict. |
required |
out_gpkg |
str |
The output GeoPackage path. |
required |
Source code in leafmap/common.py
def geojson_to_gpkg(in_geojson, out_gpkg, **kwargs):
"""Converts a GeoJSON object to GeoPackage.
Args:
in_geojson (str | dict): The input GeoJSON file or dict.
out_gpkg (str): The output GeoPackage path.
"""
import geopandas as gpd
import json
ext = os.path.splitext(out_gpkg)[1]
if ext.lower() != ".gpkg":
out_gpkg = out_gpkg + ".gpkg"
out_gpkg = check_file_path(out_gpkg)
if isinstance(in_geojson, dict):
out_file = temp_file_path(extension="geojson")
with open(out_file, "w") as f:
json.dump(in_geojson, f)
in_geojson = out_file
gdf = gpd.read_file(in_geojson, **kwargs)
name = os.path.splitext(os.path.basename(out_gpkg))[0]
gdf.to_file(out_gpkg, layer=name, driver="GPKG")
geojson_to_mbtiles(input_file, output_file, layer_name=None, options=None, quiet=False)
¶
Converts vector data to .mbtiles using Tippecanoe.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
input_file |
str |
Path to the input vector data file (e.g., .geojson). |
required |
output_file |
str |
Path to the output .mbtiles file. |
required |
layer_name |
Optional[str] |
Optional name for the layer. Defaults to None. |
None |
options |
Optional[List[str]] |
List of additional arguments for tippecanoe. For example '-zg' for auto maxzoom. Defaults to None. |
None |
quiet |
bool |
If True, suppress the log output. Defaults to False. |
False |
Returns:
Type | Description |
---|---|
Optional[str] |
Output from the Tippecanoe command, or None if there was an error or if Tippecanoe is not installed. |
Exceptions:
Type | Description |
---|---|
subprocess.CalledProcessError |
If there's an error executing the tippecanoe command. |
Source code in leafmap/common.py
def geojson_to_mbtiles(
input_file: str,
output_file: str,
layer_name: Optional[str] = None,
options: Optional[List[str]] = None,
quiet: bool = False,
) -> Optional[str]:
"""
Converts vector data to .mbtiles using Tippecanoe.
Args:
input_file (str): Path to the input vector data file (e.g., .geojson).
output_file (str): Path to the output .mbtiles file.
layer_name (Optional[str]): Optional name for the layer. Defaults to None.
options (Optional[List[str]]): List of additional arguments for tippecanoe. For example '-zg' for auto maxzoom. Defaults to None.
quiet (bool): If True, suppress the log output. Defaults to False.
Returns:
Optional[str]: Output from the Tippecanoe command, or None if there was an error or if Tippecanoe is not installed.
Raises:
subprocess.CalledProcessError: If there's an error executing the tippecanoe command.
"""
import subprocess
import shutil
# Check if tippecanoe exists
if shutil.which("tippecanoe") is None:
print("Error: tippecanoe is not installed.")
print("You can install it using conda with the following command:")
print("conda install -c conda-forge tippecanoe")
return None
command = ["tippecanoe", "-o", output_file]
# Add layer name specification if provided
if layer_name:
command.extend(["-L", f"{layer_name}:{input_file}"])
else:
command.append(input_file)
# Append additional arguments if provided
if options:
command.extend(options)
try:
process = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
)
if not quiet:
for line in process.stdout:
print(line, end="")
exit_code = process.wait()
if exit_code != 0:
raise subprocess.CalledProcessError(exit_code, command)
except subprocess.CalledProcessError as e:
print(f"\nError executing tippecanoe: {e}")
return None
return "Tippecanoe process completed successfully."
geojson_to_pmtiles(input_file, output_file=None, layer_name=None, projection='EPSG:4326', overwrite=False, options=None, quiet=False)
¶
Converts vector data to PMTiles using Tippecanoe.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
input_file |
str |
Path to the input vector data file (e.g., .geojson). |
required |
output_file |
str |
Path to the output .mbtiles file. |
None |
layer_name |
Optional[str] |
Optional name for the layer. Defaults to None. |
None |
projection |
Optional[str] |
Projection for the output PMTiles file. Defaults to "EPSG:4326". |
'EPSG:4326' |
overwrite |
bool |
If True, overwrite the existing output file. Defaults to False. |
False |
options |
Optional[List[str]] |
List of additional arguments for tippecanoe. Defaults to None. To reduce the size of the output file, use '-zg' or '-z max-zoom'. |
None |
quiet |
bool |
If True, suppress the log output. Defaults to False. |
False |
Returns:
Type | Description |
---|---|
Optional[str] |
Output from the Tippecanoe command, or None if there was an error or if Tippecanoe is not installed. |
Exceptions:
Type | Description |
---|---|
subprocess.CalledProcessError |
If there's an error executing the tippecanoe command. |
Source code in leafmap/common.py
def geojson_to_pmtiles(
input_file: str,
output_file: Optional[str] = None,
layer_name: Optional[str] = None,
projection: Optional[str] = "EPSG:4326",
overwrite: bool = False,
options: Optional[List[str]] = None,
quiet: bool = False,
) -> Optional[str]:
"""
Converts vector data to PMTiles using Tippecanoe.
Args:
input_file (str): Path to the input vector data file (e.g., .geojson).
output_file (str): Path to the output .mbtiles file.
layer_name (Optional[str]): Optional name for the layer. Defaults to None.
projection (Optional[str]): Projection for the output PMTiles file. Defaults to "EPSG:4326".
overwrite (bool): If True, overwrite the existing output file. Defaults to False.
options (Optional[List[str]]): List of additional arguments for tippecanoe. Defaults to None.
To reduce the size of the output file, use '-zg' or '-z max-zoom'.
quiet (bool): If True, suppress the log output. Defaults to False.
Returns:
Optional[str]: Output from the Tippecanoe command, or None if there was an error or if Tippecanoe is not installed.
Raises:
subprocess.CalledProcessError: If there's an error executing the tippecanoe command.
"""
import subprocess
import shutil
# Check if tippecanoe exists
if shutil.which("tippecanoe") is None:
print("Error: tippecanoe is not installed.")
print("You can install it using conda with the following command:")
print("conda install -c conda-forge tippecanoe")
return None
if output_file is None:
output_file = os.path.splitext(input_file)[0] + ".pmtiles"
if not output_file.endswith(".pmtiles"):
raise ValueError("Error: output file must be a .pmtiles file.")
command = ["tippecanoe", "-o", output_file]
# Add layer name specification if provided
if layer_name:
command.extend(["-L", f"{layer_name}:{input_file}"])
else:
command.append(input_file)
command.extend(["--projection", projection])
if options is None:
options = []
if overwrite:
command.append("--force")
# Append additional arguments if provided
if options:
command.extend(options)
try:
process = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
)
if not quiet:
for line in process.stdout:
print(line, end="")
exit_code = process.wait()
if exit_code != 0:
raise subprocess.CalledProcessError(exit_code, command)
except subprocess.CalledProcessError as e:
print(f"\nError executing tippecanoe: {e}")
return None
return "Tippecanoe process completed successfully."
geojson_to_shp(in_geojson, out_shp, **kwargs)
¶
Converts a GeoJSON object to GeoPandas GeoDataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_geojson |
str | dict |
The input GeoJSON file or dict. |
required |
out_shp |
str |
The output shapefile path. |
required |
Source code in leafmap/common.py
def geojson_to_shp(in_geojson, out_shp, **kwargs):
"""Converts a GeoJSON object to GeoPandas GeoDataFrame.
Args:
in_geojson (str | dict): The input GeoJSON file or dict.
out_shp (str): The output shapefile path.
"""
import geopandas as gpd
import json
ext = os.path.splitext(out_shp)[1]
if ext != ".shp":
out_shp = out_shp + ".shp"
out_shp = check_file_path(out_shp)
if isinstance(in_geojson, dict):
out_file = temp_file_path(extension="geojson")
with open(out_file, "w") as f:
json.dump(in_geojson, f)
in_geojson = out_file
gdf = gpd.read_file(in_geojson, **kwargs)
gdf.to_file(out_shp)
geom_type(in_geojson, encoding='utf-8')
¶
Returns the geometry type of a GeoJSON object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_geojson |
dict |
A GeoJSON object. |
required |
encoding |
str |
The encoding of the GeoJSON object. Defaults to "utf-8". |
'utf-8' |
Returns:
Type | Description |
---|---|
str |
The geometry type of the GeoJSON object, such as Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon. For more info, see https://shapely.readthedocs.io/en/stable/manual.html |
Source code in leafmap/common.py
def geom_type(in_geojson, encoding="utf-8"):
"""Returns the geometry type of a GeoJSON object.
Args:
in_geojson (dict): A GeoJSON object.
encoding (str, optional): The encoding of the GeoJSON object. Defaults to "utf-8".
Returns:
str: The geometry type of the GeoJSON object, such as Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon.
For more info, see https://shapely.readthedocs.io/en/stable/manual.html
"""
import json
try:
if isinstance(in_geojson, str):
if in_geojson.startswith("http"):
data = requests.get(in_geojson).json()
else:
in_geojson = os.path.abspath(in_geojson)
if not os.path.exists(in_geojson):
raise FileNotFoundError(
"The provided GeoJSON file could not be found."
)
with open(in_geojson, encoding=encoding) as f:
data = json.load(f)
elif isinstance(in_geojson, dict):
data = in_geojson
else:
raise TypeError("The input geojson must be a type of str or dict.")
return data["features"][0]["geometry"]["type"]
except Exception as e:
raise Exception(e)
geometry_bounds(geometry, decimals=4)
¶
Returns the bounds of a geometry.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
geometry |
dict |
A GeoJSON geometry. |
required |
decimals |
int |
The number of decimal places to round the bounds to. Defaults to 4. |
4 |
Returns:
Type | Description |
---|---|
list |
A list of bounds in the form of [minx, miny, maxx, maxy]. |
Source code in leafmap/common.py
def geometry_bounds(geometry, decimals=4):
"""Returns the bounds of a geometry.
Args:
geometry (dict): A GeoJSON geometry.
decimals (int, optional): The number of decimal places to round the bounds to. Defaults to 4.
Returns:
list: A list of bounds in the form of [minx, miny, maxx, maxy].
"""
if isinstance(geometry, dict):
if "geometry" in geometry:
coords = geometry["geometry"]["coordinates"][0]
else:
coords = geometry["coordinates"][0]
else:
raise ValueError("geometry must be a GeoJSON-like dictionary.")
x = [p[0] for p in coords]
y = [p[1] for p in coords]
west = round(min(x), decimals)
east = round(max(x), decimals)
south = round(min(y), decimals)
north = round(max(y), decimals)
return [west, south, east, north]
get_3dep_dem(geometry, resolution=30, src_crs=None, output=None, dst_crs='EPSG:5070', to_cog=False, overwrite=False, **kwargs)
¶
Get DEM data at any resolution from 3DEP.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
geometry |
Polygon | MultiPolygon | tuple |
It can be a polygon or a bounding box of form (xmin, ymin, xmax, ymax). |
required |
resolution |
int |
arget DEM source resolution in meters. Defaults to 30. |
30 |
src_crs |
str |
The spatial reference system of the input geometry. Defaults to "EPSG:4326". |
None |
output |
str |
The output GeoTIFF file. Defaults to None. |
None |
dst_crs |
str |
The spatial reference system of the output GeoTIFF file. Defaults to "EPSG:5070". |
'EPSG:5070' |
to_cog |
bool |
Convert to Cloud Optimized GeoTIFF. Defaults to False. |
False |
overwrite |
bool |
Whether to overwrite the output file if it exists. Defaults to False. |
False |
Returns:
Type | Description |
---|---|
xarray.DataArray |
DEM at the specified resolution in meters and CRS. |
Source code in leafmap/common.py
def get_3dep_dem(
geometry,
resolution=30,
src_crs=None,
output=None,
dst_crs="EPSG:5070",
to_cog=False,
overwrite=False,
**kwargs,
):
"""Get DEM data at any resolution from 3DEP.
Args:
geometry (Polygon | MultiPolygon | tuple): It can be a polygon or a bounding
box of form (xmin, ymin, xmax, ymax).
resolution (int): arget DEM source resolution in meters. Defaults to 30.
src_crs (str, optional): The spatial reference system of the input geometry. Defaults to "EPSG:4326".
output (str, optional): The output GeoTIFF file. Defaults to None.
dst_crs (str, optional): The spatial reference system of the output GeoTIFF file. Defaults to "EPSG:5070".
to_cog (bool, optional): Convert to Cloud Optimized GeoTIFF. Defaults to False.
overwrite (bool, optional): Whether to overwrite the output file if it exists. Defaults to False.
Returns:
xarray.DataArray: DEM at the specified resolution in meters and CRS.
"""
try:
import py3dep
except ImportError:
print("py3dep is not installed. Installing py3dep...")
install_package("py3dep")
import py3dep
import geopandas as gpd
if output is not None and os.path.exists(output) and not overwrite:
print(f"File {output} already exists. Set overwrite=True to overwrite it")
return
if isinstance(geometry, gpd.GeoDataFrame):
if src_crs is None:
src_crs = geometry.crs
geometry = geometry.geometry.unary_union
if src_crs is None:
src_crs = "EPSG:4326"
dem = py3dep.get_dem(geometry, resolution=resolution, crs=src_crs)
dem = dem.rio.reproject(dst_crs)
if output is not None:
if not output.endswith(".tif"):
output += ".tif"
dem.rio.to_raster(output, **kwargs)
if to_cog:
try:
image_to_cog(output, output)
except Exception as e:
print(e)
return dem
get_api_key(name=None, key=None)
¶
Retrieves an API key. If a key is provided, it is returned directly. If a name is provided, the function attempts to retrieve the key from user data (if running in Google Colab) or from environment variables.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
name |
Optional[str] |
The name of the key to retrieve. Defaults to None. |
None |
key |
Optional[str] |
The key to return directly. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
Optional[str] |
The retrieved key, or None if no key was found. |
Source code in leafmap/common.py
def get_api_key(name: Optional[str] = None, key: Optional[str] = None) -> Optional[str]:
"""
Retrieves an API key. If a key is provided, it is returned directly. If a
name is provided, the function attempts to retrieve the key from user data
(if running in Google Colab) or from environment variables.
Args:
name (Optional[str], optional): The name of the key to retrieve. Defaults to None.
key (Optional[str], optional): The key to return directly. Defaults to None.
Returns:
Optional[str]: The retrieved key, or None if no key was found.
"""
if key is not None:
return key
elif name is not None:
if _in_colab_shell():
from google.colab import userdata # pylint: disable=E0611
try:
return userdata.get(name)
except:
return os.environ.get(name)
else:
return os.environ.get(name)
get_bounds(geometry, north_up=True, transform=None)
¶
Bounding box of a GeoJSON geometry, GeometryCollection, or FeatureCollection. left, bottom, right, top not xmin, ymin, xmax, ymax If not north_up, y will be switched to guarantee the above. Source code adapted from https://github.com/mapbox/rasterio/blob/master/rasterio/features.py#L361
Parameters:
Name | Type | Description | Default |
---|---|---|---|
geometry |
dict |
A GeoJSON dict. |
required |
north_up |
bool |
. Defaults to True. |
True |
transform |
[type] |
. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
list |
A list of coordinates representing [left, bottom, right, top] |
Source code in leafmap/common.py
def get_bounds(geometry, north_up=True, transform=None):
"""Bounding box of a GeoJSON geometry, GeometryCollection, or FeatureCollection.
left, bottom, right, top
*not* xmin, ymin, xmax, ymax
If not north_up, y will be switched to guarantee the above.
Source code adapted from https://github.com/mapbox/rasterio/blob/master/rasterio/features.py#L361
Args:
geometry (dict): A GeoJSON dict.
north_up (bool, optional): . Defaults to True.
transform ([type], optional): . Defaults to None.
Returns:
list: A list of coordinates representing [left, bottom, right, top]
"""
if "bbox" in geometry:
return tuple(geometry["bbox"])
geometry = geometry.get("geometry") or geometry
# geometry must be a geometry, GeometryCollection, or FeatureCollection
if not (
"coordinates" in geometry or "geometries" in geometry or "features" in geometry
):
raise ValueError(
"geometry must be a GeoJSON-like geometry, GeometryCollection, "
"or FeatureCollection"
)
if "features" in geometry:
# Input is a FeatureCollection
xmins = []
ymins = []
xmaxs = []
ymaxs = []
for feature in geometry["features"]:
xmin, ymin, xmax, ymax = get_bounds(feature["geometry"])
xmins.append(xmin)
ymins.append(ymin)
xmaxs.append(xmax)
ymaxs.append(ymax)
if north_up:
return min(xmins), min(ymins), max(xmaxs), max(ymaxs)
else:
return min(xmins), max(ymaxs), max(xmaxs), min(ymins)
elif "geometries" in geometry:
# Input is a geometry collection
xmins = []
ymins = []
xmaxs = []
ymaxs = []
for geometry in geometry["geometries"]:
xmin, ymin, xmax, ymax = get_bounds(geometry)
xmins.append(xmin)
ymins.append(ymin)
xmaxs.append(xmax)
ymaxs.append(ymax)
if north_up:
return min(xmins), min(ymins), max(xmaxs), max(ymaxs)
else:
return min(xmins), max(ymaxs), max(xmaxs), min(ymins)
elif "coordinates" in geometry:
# Input is a singular geometry object
if transform is not None:
xyz = list(explode(geometry["coordinates"]))
xyz_px = [transform * point for point in xyz]
xyz = tuple(zip(*xyz_px))
return min(xyz[0]), max(xyz[1]), max(xyz[0]), min(xyz[1])
else:
xyz = tuple(zip(*list(explode(geometry["coordinates"]))))
if north_up:
return min(xyz[0]), min(xyz[1]), max(xyz[0]), max(xyz[1])
else:
return min(xyz[0]), max(xyz[1]), max(xyz[0]), min(xyz[1])
# all valid inputs returned above, so whatever falls through is an error
raise ValueError(
"geometry must be a GeoJSON-like geometry, GeometryCollection, "
"or FeatureCollection"
)
get_census_dict(reset=False)
¶
Returns a dictionary of Census data.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
reset |
bool |
Reset the dictionary. Defaults to False. |
False |
Returns:
Type | Description |
---|---|
dict |
A dictionary of Census data. |
Source code in leafmap/common.py
def get_census_dict(reset=False):
"""Returns a dictionary of Census data.
Args:
reset (bool, optional): Reset the dictionary. Defaults to False.
Returns:
dict: A dictionary of Census data.
"""
import json
import pkg_resources
pkg_dir = os.path.dirname(pkg_resources.resource_filename("leafmap", "leafmap.py"))
census_data = os.path.join(pkg_dir, "data/census_data.json")
if reset:
try:
from owslib.wms import WebMapService
except ImportError:
raise ImportError("Please install owslib using 'pip install owslib'.")
census_dict = {}
names = [
"Current",
"ACS 2021",
"ACS 2019",
"ACS 2018",
"ACS 2017",
"ACS 2016",
"ACS 2015",
"ACS 2014",
"ACS 2013",
"ACS 2012",
"ECON 2012",
"Census 2020",
"Census 2010",
"Physical Features",
"Decennial Census 2020",
"Decennial Census 2010",
"Decennial Census 2000",
"Decennial Physical Features",
]
links = {}
print("Retrieving data. Please wait ...")
for name in names:
if "Decennial" not in name:
links[name] = (
f"https://tigerweb.geo.census.gov/arcgis/services/TIGERweb/tigerWMS_{name.replace(' ', '')}/MapServer/WMSServer"
)
else:
links[name] = (
f"https://tigerweb.geo.census.gov/arcgis/services/Census2020/tigerWMS_{name.replace('Decennial', '').replace(' ', '')}/MapServer/WMSServer"
)
wms = WebMapService(links[name], timeout=300)
layers = list(wms.contents)
layers.sort()
census_dict[name] = {
"url": links[name],
"layers": layers,
# "title": wms.identification.title,
# "abstract": wms.identification.abstract,
}
with open(census_data, "w") as f:
json.dump(census_dict, f, indent=4)
else:
with open(census_data, "r") as f:
census_dict = json.load(f)
return census_dict
get_center(geometry, north_up=True, transform=None)
¶
Get the centroid of a GeoJSON.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
geometry |
dict |
A GeoJSON dict. |
required |
north_up |
bool |
. Defaults to True. |
True |
transform |
[type] |
. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
list |
[lon, lat] |
Source code in leafmap/common.py
def get_center(geometry, north_up=True, transform=None):
"""Get the centroid of a GeoJSON.
Args:
geometry (dict): A GeoJSON dict.
north_up (bool, optional): . Defaults to True.
transform ([type], optional): . Defaults to None.
Returns:
list: [lon, lat]
"""
bounds = get_bounds(geometry, north_up, transform)
center = ((bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2) # (lat, lon)
return center
get_direct_url(url)
¶
Get the direct URL for a given URL.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
The URL to get the direct URL for. |
required |
Returns:
Type | Description |
---|---|
str |
The direct URL. |
Source code in leafmap/common.py
def get_direct_url(url):
"""Get the direct URL for a given URL.
Args:
url (str): The URL to get the direct URL for.
Returns:
str: The direct URL.
"""
if not isinstance(url, str):
raise ValueError("url must be a string.")
if not url.startswith("http"):
raise ValueError("url must start with http.")
r = requests.head(url, allow_redirects=True)
return r.url
get_gdal_drivers()
¶
Get a list of available driver names in the GDAL library.
Returns:
Type | Description |
---|---|
List[str] |
A list of available driver names. |
Source code in leafmap/common.py
def get_gdal_drivers() -> List[str]:
"""Get a list of available driver names in the GDAL library.
Returns:
List[str]: A list of available driver names.
"""
from osgeo import ogr
driver_list = []
# Iterate over all registered drivers
for i in range(ogr.GetDriverCount()):
driver = ogr.GetDriver(i)
driver_name = driver.GetName()
driver_list.append(driver_name)
return driver_list
get_gdal_file_extension(driver_name)
¶
Get the file extension corresponding to a driver name in the GDAL library.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
driver_name |
str |
The name of the driver. |
required |
Returns:
Type | Description |
---|---|
Optional[str] |
The file extension corresponding to the driver name, or None if the driver is not found or does not have a specific file extension. |
Source code in leafmap/common.py
def get_gdal_file_extension(driver_name: str) -> Optional[str]:
"""Get the file extension corresponding to a driver name in the GDAL library.
Args:
driver_name (str): The name of the driver.
Returns:
Optional[str]: The file extension corresponding to the driver name, or None if the driver is not found or does not have a specific file extension.
"""
from osgeo import ogr
driver = ogr.GetDriverByName(driver_name)
if driver is None:
drivers = get_gdal_drivers()
raise ValueError(
f"Driver {driver_name} not found. Available drivers: {drivers}"
)
metadata = driver.GetMetadata()
if "DMD_EXTENSION" in metadata:
file_extension = driver.GetMetadataItem("DMD_EXTENSION")
else:
file_extensions = driver.GetMetadataItem("DMD_EXTENSIONS")
if file_extensions == "json geojson":
file_extension = "geojson"
else:
file_extension = file_extensions.split()[0].lower()
return file_extension
get_geometry_coords(row, geom, coord_type, shape_type, mercator=False)
¶
Returns the coordinates ('x' or 'y') of edges of a Polygon exterior.
:param: (GeoPandas Series) row : The row of each of the GeoPandas DataFrame. :param: (str) geom : The column name. :param: (str) coord_type : Whether it's 'x' or 'y' coordinate. :param: (str) shape_type
Source code in leafmap/common.py
def get_geometry_coords(row, geom, coord_type, shape_type, mercator=False):
"""
Returns the coordinates ('x' or 'y') of edges of a Polygon exterior.
:param: (GeoPandas Series) row : The row of each of the GeoPandas DataFrame.
:param: (str) geom : The column name.
:param: (str) coord_type : Whether it's 'x' or 'y' coordinate.
:param: (str) shape_type
"""
# Parse the exterior of the coordinate
if shape_type.lower() in ["polygon", "multipolygon"]:
exterior = row[geom].geoms[0].exterior
if coord_type == "x":
# Get the x coordinates of the exterior
coords = list(exterior.coords.xy[0])
if mercator:
coords = [lnglat_to_meters(x, 0)[0] for x in coords]
return coords
elif coord_type == "y":
# Get the y coordinates of the exterior
coords = list(exterior.coords.xy[1])
if mercator:
coords = [lnglat_to_meters(0, y)[1] for y in coords]
return coords
elif shape_type.lower() in ["linestring", "multilinestring"]:
if coord_type == "x":
coords = list(row[geom].coords.xy[0])
if mercator:
coords = [lnglat_to_meters(x, 0)[0] for x in coords]
return coords
elif coord_type == "y":
coords = list(row[geom].coords.xy[1])
if mercator:
coords = [lnglat_to_meters(0, y)[1] for y in coords]
return coords
elif shape_type.lower() in ["point", "multipoint"]:
exterior = row[geom]
if coord_type == "x":
# Get the x coordinates of the exterior
coords = exterior.coords.xy[0][0]
if mercator:
coords = lnglat_to_meters(coords, 0)[0]
return coords
elif coord_type == "y":
# Get the y coordinates of the exterior
coords = exterior.coords.xy[1][0]
if mercator:
coords = lnglat_to_meters(0, coords)[1]
return coords
get_geometry_type(in_geojson)
¶
Get the geometry type of a GeoJSON file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_geojson |
str | dict |
The path to the GeoJSON file or a GeoJSON dictionary. |
required |
Returns:
Type | Description |
---|---|
str |
The geometry type. Can be one of "Point", "LineString", "Polygon", "MultiPoint", "MultiLineString", "MultiPolygon", "GeometryCollection", or "Unknown". |
Source code in leafmap/common.py
def get_geometry_type(in_geojson: Union[str, Dict]) -> str:
"""Get the geometry type of a GeoJSON file.
Args:
in_geojson (str | dict): The path to the GeoJSON file or a GeoJSON dictionary.
Returns:
str: The geometry type. Can be one of "Point", "LineString", "Polygon", "MultiPoint",
"MultiLineString", "MultiPolygon", "GeometryCollection", or "Unknown".
"""
import geojson
try:
if isinstance(in_geojson, str): # If input is a file path
with open(in_geojson, "r") as geojson_file:
geojson_data = geojson.load(geojson_file)
elif isinstance(in_geojson, dict): # If input is a GeoJSON dictionary
geojson_data = in_geojson
else:
return "Invalid input type. Expected file path or dictionary."
if "type" in geojson_data:
if geojson_data["type"] == "FeatureCollection":
features = geojson_data.get("features", [])
if features:
first_feature = features[0]
geometry = first_feature.get("geometry")
if geometry and "type" in geometry:
return geometry["type"]
else:
return "No geometry type found in the first feature."
else:
return "No features found in the FeatureCollection."
elif geojson_data["type"] == "Feature":
geometry = geojson_data.get("geometry")
if geometry and "type" in geometry:
return geometry["type"]
else:
return "No geometry type found in the Feature."
else:
return "Unsupported GeoJSON type."
else:
return "No 'type' field found in the GeoJSON data."
except Exception as e:
raise e
get_google_map(map_type='HYBRID', show=True, api_key=None, backend='ipyleaflet', **kwargs)
¶
Gets Google basemap tile layer.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
map_type |
str |
Can be one of "ROADMAP", "SATELLITE", "HYBRID" or "TERRAIN". Defaults to 'HYBRID'. |
'HYBRID' |
show |
bool |
Whether to add the layer to the map. Defaults to True. |
True |
api_key |
str |
The Google Maps API key. Defaults to None. |
None |
**kwargs |
Additional arguments to pass to ipyleaflet.TileLayer(). |
{} |
Source code in leafmap/common.py
def get_google_map(
map_type="HYBRID", show=True, api_key=None, backend="ipyleaflet", **kwargs
):
"""Gets Google basemap tile layer.
Args:
map_type (str, optional): Can be one of "ROADMAP", "SATELLITE", "HYBRID" or "TERRAIN". Defaults to 'HYBRID'.
show (bool, optional): Whether to add the layer to the map. Defaults to True.
api_key (str, optional): The Google Maps API key. Defaults to None.
**kwargs: Additional arguments to pass to ipyleaflet.TileLayer().
"""
allow_types = ["ROADMAP", "SATELLITE", "HYBRID", "TERRAIN"]
if map_type not in allow_types:
print("map_type must be one of the following: {}".format(allow_types))
return
if api_key is None:
api_key = os.environ.get("GOOGLE_MAPS_API_KEY", "YOUR-API-KEY")
if api_key == "":
MAP_TILES = {
"ROADMAP": {
"url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}",
"attribution": "Esri",
"name": "Esri.WorldStreetMap",
},
"SATELLITE": {
"url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
"attribution": "Esri",
"name": "Esri.WorldImagery",
},
"TERRAIN": {
"url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}",
"attribution": "Esri",
"name": "Esri.WorldTopoMap",
},
"HYBRID": {
"url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
"attribution": "Esri",
"name": "Esri.WorldImagery",
},
}
print(
"Google Maps API key is required to use Google Maps. You can generate one from https://bit.ly/3sw0THG and use geemap.set_api_key(), defaulting to Esri basemaps."
)
else:
MAP_TILES = {
"ROADMAP": {
"url": f"https://mt1.google.com/vt/lyrs=m&x={{x}}&y={{y}}&z={{z}}&key={api_key}",
"attribution": "Google",
"name": "Google Maps",
},
"SATELLITE": {
"url": f"https://mt1.google.com/vt/lyrs=s&x={{x}}&y={{y}}&z={{z}}&key={api_key}",
"attribution": "Google",
"name": "Google Satellite",
},
"TERRAIN": {
"url": f"https://mt1.google.com/vt/lyrs=p&x={{x}}&y={{y}}&z={{z}}&key={api_key}",
"attribution": "Google",
"name": "Google Terrain",
},
"HYBRID": {
"url": f"https://mt1.google.com/vt/lyrs=y&x={{x}}&y={{y}}&z={{z}}&key={api_key}",
"attribution": "Google",
"name": "Google Hybrid",
},
}
if "max_zoom" not in kwargs:
kwargs["max_zoom"] = 24
if backend == "ipyleaflet":
import ipyleaflet
layer = ipyleaflet.TileLayer(
url=MAP_TILES[map_type]["url"],
name=MAP_TILES[map_type]["name"],
attribution=MAP_TILES[map_type]["attribution"],
visible=show,
**kwargs,
)
elif backend == "folium":
import folium
layer = folium.TileLayer(
tiles=MAP_TILES[map_type]["url"],
name=MAP_TILES[map_type]["name"],
attr=MAP_TILES[map_type]["attribution"],
overlay=True,
control=True,
show=show,
**kwargs,
)
else:
raise ValueError("backend must be either 'ipyleaflet' or 'folium'")
return layer
get_local_tile_layer(source, port='default', debug=False, indexes=None, colormap=None, vmin=None, vmax=None, nodata=None, attribution=None, tile_format='ipyleaflet', layer_name='Local COG', client_args={'cors_all': False}, return_client=False, quiet=False, **kwargs)
¶
Generate an ipyleaflet/folium TileLayer from a local raster dataset or remote Cloud Optimized GeoTIFF (COG). If you are using this function in JupyterHub on a remote server and the raster does not render properly, try running the following two lines before calling this function:
1 2 |
|
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source |
str |
The path to the GeoTIFF file or the URL of the Cloud Optimized GeoTIFF. |
required |
port |
str |
The port to use for the server. Defaults to "default". |
'default' |
debug |
bool |
If True, the server will be started in debug mode. Defaults to False. |
False |
indexes |
int |
The band(s) to use. Band indexing starts at 1. Defaults to None. |
None |
colormap |
str |
The name of the colormap from |
None |
vmin |
float |
The minimum value to use when colormapping the colormap when plotting a single band. Defaults to None. |
None |
vmax |
float |
The maximum value to use when colormapping the colormap when plotting a single band. Defaults to None. |
None |
nodata |
float |
The value from the band to use to interpret as not valid data. Defaults to None. |
None |
attribution |
str |
Attribution for the source raster. This defaults to a message about it being a local file.. Defaults to None. |
None |
tile_format |
str |
The tile layer format. Can be either ipyleaflet or folium. Defaults to "ipyleaflet". |
'ipyleaflet' |
layer_name |
str |
The layer name to use. Defaults to None. |
'Local COG' |
client_args |
dict |
Additional arguments to pass to the TileClient. Defaults to {}. |
{'cors_all': False} |
return_client |
bool |
If True, the tile client will be returned. Defaults to False. |
False |
quiet |
bool |
If True, the error messages will be suppressed. Defaults to False. |
False |
Returns:
Type | Description |
---|---|
ipyleaflet.TileLayer | folium.TileLayer |
An ipyleaflet.TileLayer or folium.TileLayer. |
Source code in leafmap/common.py
def get_local_tile_layer(
source,
port="default",
debug=False,
indexes=None,
colormap=None,
vmin=None,
vmax=None,
nodata=None,
attribution=None,
tile_format="ipyleaflet",
layer_name="Local COG",
client_args={"cors_all": False},
return_client=False,
quiet=False,
**kwargs,
):
"""Generate an ipyleaflet/folium TileLayer from a local raster dataset or remote Cloud Optimized GeoTIFF (COG).
If you are using this function in JupyterHub on a remote server and the raster does not render properly, try
running the following two lines before calling this function:
import os
os.environ['LOCALTILESERVER_CLIENT_PREFIX'] = 'proxy/{port}'
Args:
source (str): The path to the GeoTIFF file or the URL of the Cloud Optimized GeoTIFF.
port (str, optional): The port to use for the server. Defaults to "default".
debug (bool, optional): If True, the server will be started in debug mode. Defaults to False.
indexes (int, optional): The band(s) to use. Band indexing starts at 1. Defaults to None.
colormap (str, optional): The name of the colormap from `matplotlib` to use when plotting a single band. See https://matplotlib.org/stable/gallery/color/colormap_reference.html. Default is greyscale.
vmin (float, optional): The minimum value to use when colormapping the colormap when plotting a single band. Defaults to None.
vmax (float, optional): The maximum value to use when colormapping the colormap when plotting a single band. Defaults to None.
nodata (float, optional): The value from the band to use to interpret as not valid data. Defaults to None.
attribution (str, optional): Attribution for the source raster. This defaults to a message about it being a local file.. Defaults to None.
tile_format (str, optional): The tile layer format. Can be either ipyleaflet or folium. Defaults to "ipyleaflet".
layer_name (str, optional): The layer name to use. Defaults to None.
client_args (dict, optional): Additional arguments to pass to the TileClient. Defaults to {}.
return_client (bool, optional): If True, the tile client will be returned. Defaults to False.
quiet (bool, optional): If True, the error messages will be suppressed. Defaults to False.
Returns:
ipyleaflet.TileLayer | folium.TileLayer: An ipyleaflet.TileLayer or folium.TileLayer.
"""
import rasterio
check_package(
"localtileserver", URL="https://github.com/banesullivan/localtileserver"
)
# Handle legacy localtileserver kwargs
if "cmap" in kwargs:
warnings.warn(
"`cmap` is a deprecated keyword argument for get_local_tile_layer. Please use `colormap`."
)
if "palette" in kwargs:
warnings.warn(
"`palette` is a deprecated keyword argument for get_local_tile_layer. Please use `colormap`."
)
if "band" in kwargs or "bands" in kwargs:
warnings.warn(
"`band` and `bands` are deprecated keyword arguments for get_local_tile_layer. Please use `indexes`."
)
if "projection" in kwargs:
warnings.warn(
"`projection` is a deprecated keyword argument for get_local_tile_layer and will be ignored."
)
if "style" in kwargs:
warnings.warn(
"`style` is a deprecated keyword argument for get_local_tile_layer and will be ignored."
)
if "max_zoom" not in kwargs:
kwargs["max_zoom"] = 30
if "max_native_zoom" not in kwargs:
kwargs["max_native_zoom"] = 30
if "cmap" in kwargs:
colormap = kwargs.pop("cmap")
if "palette" in kwargs:
colormap = kwargs.pop("palette")
if "band" in kwargs:
indexes = kwargs.pop("band")
if "bands" in kwargs:
indexes = kwargs.pop("bands")
# Make it compatible with binder and JupyterHub
if os.environ.get("JUPYTERHUB_SERVICE_PREFIX") is not None:
os.environ["LOCALTILESERVER_CLIENT_PREFIX"] = (
f"{os.environ['JUPYTERHUB_SERVICE_PREFIX'].lstrip('/')}/proxy/{{port}}"
)
if is_studio_lab():
os.environ["LOCALTILESERVER_CLIENT_PREFIX"] = (
f"studiolab/default/jupyter/proxy/{{port}}"
)
elif is_on_aws():
os.environ["LOCALTILESERVER_CLIENT_PREFIX"] = "proxy/{port}"
elif "prefix" in kwargs:
os.environ["LOCALTILESERVER_CLIENT_PREFIX"] = kwargs["prefix"]
kwargs.pop("prefix")
from localtileserver import (
get_leaflet_tile_layer,
get_folium_tile_layer,
TileClient,
)
# if "show_loading" not in kwargs:
# kwargs["show_loading"] = False
if isinstance(source, str):
if not source.startswith("http"):
if source.startswith("~"):
source = os.path.expanduser(source)
# else:
# source = os.path.abspath(source)
# if not os.path.exists(source):
# raise ValueError("The source path does not exist.")
else:
source = github_raw_url(source)
elif isinstance(source, TileClient) or isinstance(
source, rasterio.io.DatasetReader
):
pass
else:
raise ValueError("The source must either be a string or TileClient")
if tile_format not in ["ipyleaflet", "folium"]:
raise ValueError("The tile format must be either ipyleaflet or folium.")
if layer_name is None:
if source.startswith("http"):
layer_name = "RemoteTile_" + random_string(3)
else:
layer_name = "LocalTile_" + random_string(3)
if isinstance(source, str) or isinstance(source, rasterio.io.DatasetReader):
tile_client = TileClient(source, port=port, debug=debug, **client_args)
else:
tile_client = source
if nodata is None:
nodata = get_api_key("NODATA")
if isinstance(nodata, str):
nodata = float(nodata)
if quiet:
output = widgets.Output()
with output:
if tile_format == "ipyleaflet":
tile_layer = get_leaflet_tile_layer(
tile_client,
port=port,
debug=debug,
indexes=indexes,
colormap=colormap,
vmin=vmin,
vmax=vmax,
nodata=nodata,
attribution=attribution,
name=layer_name,
**kwargs,
)
else:
tile_layer = get_folium_tile_layer(
tile_client,
port=port,
debug=debug,
indexes=indexes,
colormap=colormap,
vmin=vmin,
vmax=vmax,
nodata=nodata,
attr=attribution,
overlay=True,
name=layer_name,
**kwargs,
)
else:
if tile_format == "ipyleaflet":
tile_layer = get_leaflet_tile_layer(
tile_client,
port=port,
debug=debug,
indexes=indexes,
colormap=colormap,
vmin=vmin,
vmax=vmax,
nodata=nodata,
attribution=attribution,
name=layer_name,
**kwargs,
)
else:
tile_layer = get_folium_tile_layer(
tile_client,
port=port,
debug=debug,
indexes=indexes,
colormap=colormap,
vmin=vmin,
vmax=vmax,
nodata=nodata,
attr=attribution,
overlay=True,
name=layer_name,
**kwargs,
)
if return_client:
return tile_layer, tile_client
else:
return tile_layer
# center = tile_client.center()
# bounds = tile_client.bounds() # [ymin, ymax, xmin, xmax]
# bounds = (bounds[2], bounds[0], bounds[3], bounds[1]) # [minx, miny, maxx, maxy]
# if get_center and get_bounds:
# return tile_layer, center, bounds
# elif get_center:
# return tile_layer, center
# elif get_bounds:
# return tile_layer, bounds
# else:
# return tile_layer
get_max_pixel_coords(geotiff_path, band_idx=1, roi=None, dst_crs='EPSG:4326', output=None, return_gdf=True, **kwargs)
¶
Find the geographic coordinates of the maximum pixel value in a GeoTIFF.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
geotiff_path |
str |
Path to the GeoTIFF file. |
required |
band_idx |
int |
Band index to use (default is 1). |
1 |
roi |
str |
Path to a vector dataset containing the region of interest (default is None). |
None |
dst_crs |
str |
Desired output coordinate system in EPSG format (e.g., "EPSG:4326"). |
'EPSG:4326' |
output |
str |
Path to save the output GeoDataFrame (default is None). |
None |
return_gdf |
bool |
Whether to return a GeoDataFrame (default is True). |
True |
Returns:
Type | Description |
---|---|
dict |
Maximum pixel value and its geographic coordinates in the specified CRS. |
Source code in leafmap/common.py
def get_max_pixel_coords(
geotiff_path,
band_idx=1,
roi=None,
dst_crs="EPSG:4326",
output=None,
return_gdf=True,
**kwargs,
):
"""
Find the geographic coordinates of the maximum pixel value in a GeoTIFF.
Args:
geotiff_path (str): Path to the GeoTIFF file.
band_idx (int): Band index to use (default is 1).
roi (str): Path to a vector dataset containing the region of interest (default is None).
dst_crs (str): Desired output coordinate system in EPSG format (e.g., "EPSG:4326").
output (str): Path to save the output GeoDataFrame (default is None).
return_gdf (bool): Whether to return a GeoDataFrame (default is True).
Returns:
dict: Maximum pixel value and its geographic coordinates in the specified CRS.
"""
import rasterio
import numpy as np
import geopandas as gpd
from rasterio.warp import transform
from rasterio.mask import mask
from rasterio.warp import transform, transform_geom
with rasterio.open(geotiff_path) as dataset:
# If ROI is provided, handle potential CRS differences
if roi:
if isinstance(roi, str):
gdf = gpd.read_file(roi)
elif isinstance(roi, gpd.GeoDataFrame):
gdf = roi
elif isinstance(roi, dict):
gdf = gpd.GeoDataFrame.from_features([roi])
else:
raise ValueError(
"Invalid ROI input. Must be a file path or a GeoDataFrame."
)
roi_geojson = gdf.__geo_interface__
# Reproject ROI to match the raster's CRS if necessary
roi_crs = gdf.crs
if roi_crs is None:
roi_crs = "EPSG:4326"
if roi_crs != dataset.crs.to_string():
roi_geojson["features"][0]["geometry"] = transform_geom(
roi_crs,
dataset.crs.to_string(),
roi_geojson["features"][0]["geometry"],
)
# Mask the raster using the transformed ROI geometry
clipped_band, clipped_transform = mask(
dataset, [roi_geojson["features"][0]["geometry"]], crop=True
)
band = clipped_band[
band_idx - 1
] # Mask returns a 3D array (bands, rows, cols), so select the first band
transform_to_use = clipped_transform
else:
# Use the entire raster
band = dataset.read(band_idx)
transform_to_use = dataset.transform
# Find the maximum value and its index
max_value = band.max()
max_index = np.unravel_index(band.argmax(), band.shape)
# Convert pixel coordinates to the raster's CRS coordinates
original_coords = transform_to_use * (max_index[1], max_index[0])
# Transform coordinates to the desired CRS
src_crs = dataset.crs
x, y = transform(src_crs, dst_crs, [original_coords[0]], [original_coords[1]])
if return_gdf:
x_coords = [x[0]]
y_coords = [y[0]]
# Create a DataFrame
df = pd.DataFrame({"x": x_coords, "y": y_coords})
# Convert the DataFrame to a GeoDataFrame
gdf = gpd.GeoDataFrame(
df, geometry=gpd.points_from_xy(df.x, df.y), crs=dst_crs
)
if output:
gdf.to_file(output, **kwargs)
else:
return {"max_value": max_value, "coordinates": (x[0], y[0]), "crs": dst_crs}
get_nhd(geometry, geo_crs=4326, xy=True, buffer=0.001, dataset='wbd08', predicate='intersects', sort_attr=None, **kwargs)
¶
Fetches National Hydrography Dataset (NHD) data based on the provided geometry.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
geometry |
Union[gpd.GeoDataFrame, str, List[float], Tuple[float, float, float, float]] |
The geometry to query the NHD data. It can be a GeoDataFrame, a file path, or coordinates. |
required |
geo_crs |
int |
The coordinate reference system (CRS) of the geometry (default is 4326). |
4326 |
xy |
bool |
Whether to use x, y coordinates (default is True). |
True |
buffer |
float |
The buffer distance around the centroid point (default is 0.001 degrees). |
0.001 |
dataset |
str |
The NHD dataset to query (default is "wbd08"). |
'wbd08' |
predicate |
str |
The spatial predicate to use for the query (default is "intersects"). |
'intersects' |
sort_attr |
Optional[str] |
The attribute to sort the results by (default is None). |
None |
**kwargs |
Additional keyword arguments to pass to the WaterData.bygeom method. |
{} |
Returns:
Type | Description |
---|---|
Optional[gpd.GeoDataFrame] |
The fetched NHD data as a GeoDataFrame, or None if an error occurs. |
Exceptions:
Type | Description |
---|---|
ImportError |
If the pynhd package is not installed. |
ValueError |
If the geometry type is unsupported. |
Source code in leafmap/common.py
def get_nhd(
geometry: Union[
"gpd.GeoDataFrame", str, List[float], Tuple[float, float, float, float]
],
geo_crs: int = 4326,
xy: bool = True,
buffer: float = 0.001,
dataset: str = "wbd08",
predicate: str = "intersects",
sort_attr: Optional[str] = None,
**kwargs,
) -> Optional["gpd.GeoDataFrame"]:
"""
Fetches National Hydrography Dataset (NHD) data based on the provided geometry.
Args:
geometry (Union[gpd.GeoDataFrame, str, List[float], Tuple[float, float, float, float]]):
The geometry to query the NHD data. It can be a GeoDataFrame, a file path, or coordinates.
geo_crs (int): The coordinate reference system (CRS) of the geometry (default is 4326).
xy (bool): Whether to use x, y coordinates (default is True).
buffer (float): The buffer distance around the centroid point (default is 0.001 degrees).
dataset (str): The NHD dataset to query (default is "wbd08").
predicate (str): The spatial predicate to use for the query (default is "intersects").
sort_attr (Optional[str]): The attribute to sort the results by (default is None).
**kwargs: Additional keyword arguments to pass to the WaterData.bygeom method.
Returns:
Optional[gpd.GeoDataFrame]: The fetched NHD data as a GeoDataFrame, or None if an error occurs.
Raises:
ImportError: If the pynhd package is not installed.
ValueError: If the geometry type is unsupported.
"""
try:
import pynhd
except ImportError:
print("The pynhd package is required for this function. Installing...")
install_package("pynhd")
import geopandas as gpd
from pynhd import WaterData
if isinstance(geometry, (list, tuple)):
crs = f"EPSG:{geo_crs}"
geometry = construct_bbox(*geometry, buffer=buffer, crs=crs, return_gdf=False)
elif isinstance(geometry, gpd.GeoDataFrame):
geometry = geometry.unary_union
elif isinstance(geometry, str):
geometry = gpd.read_file(geometry).unary_union
water_data = WaterData(dataset)
try:
gdf = water_data.bygeom(geometry, geo_crs, xy, predicate, sort_attr, **kwargs)
except Exception as e:
print(e)
gdf = None
return gdf
get_nhd_basins(feature_ids, fsource='nwissite', split_catchment=False, simplified=True, **kwargs)
¶
Get NHD basins for a list of station IDs.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
feature_ids |
str | list |
Target feature ID(s). |
required |
fsource |
str |
The name of feature(s) source, defaults to |
'nwissite' |
split_catchment |
bool |
If True, split basins at their outlet locations |
False |
simplified |
bool |
If True, return a simplified version of basin geometries. Default to True. |
True |
Exceptions:
Type | Description |
---|---|
ImportError |
If pynhd is not installed. |
Returns:
Type | Description |
---|---|
geopandas.GeoDataFrame |
NLDI indexed basins in EPSG:4326. If some IDs don't return any features a list of missing ID(s) are returned as well. |
Source code in leafmap/common.py
def get_nhd_basins(
feature_ids,
fsource="nwissite",
split_catchment=False,
simplified=True,
**kwargs,
):
"""Get NHD basins for a list of station IDs.
Args:
feature_ids (str | list): Target feature ID(s).
fsource (str, optional): The name of feature(s) source, defaults to ``nwissite``.
The valid sources are:
* 'comid' for NHDPlus comid.
* 'ca_gages' for Streamgage catalog for CA SB19
* 'gfv11_pois' for USGS Geospatial Fabric V1.1 Points of Interest
* 'huc12pp' for HUC12 Pour Points
* 'nmwdi-st' for New Mexico Water Data Initiative Sites
* 'nwisgw' for NWIS Groundwater Sites
* 'nwissite' for NWIS Surface Water Sites
* 'ref_gage' for geoconnex.us reference gauges
* 'vigil' for Vigil Network Data
* 'wade' for Water Data Exchange 2.0 Sites
* 'WQP' for Water Quality Portal
split_catchment (bool, optional): If True, split basins at their outlet locations
simplified (bool, optional): If True, return a simplified version of basin geometries.
Default to True.
Raises:
ImportError: If pynhd is not installed.
Returns:
geopandas.GeoDataFrame: NLDI indexed basins in EPSG:4326. If some IDs don't return any features
a list of missing ID(s) are returned as well.
"""
try:
from pynhd import NLDI
except ImportError:
raise ImportError("pynhd is not installed. Install it with pip install pynhd")
return NLDI().get_basins(
feature_ids, fsource, split_catchment, simplified, **kwargs
)
get_nwi(geometry, inSR='4326', outSR='3857', spatialRel='esriSpatialRelIntersects', return_geometry=True, outFields='*', output=None, **kwargs)
¶
Query the NWI (National Wetlands Inventory) API using various geometry types. https://fwspublicservices.wim.usgs.gov/wetlandsmapservice/rest/services/Wetlands/FeatureServer
Parameters:
Name | Type | Description | Default |
---|---|---|---|
geometry |
dict |
The geometry data (e.g., point, polygon, polyline, multipoint, etc.). |
required |
inSR |
str |
The input spatial reference (default is EPSG:4326). |
'4326' |
outSR |
str |
The output spatial reference (default is EPSG:3857). |
'3857' |
spatialRel |
str |
The spatial relationship (default is "esriSpatialRelIntersects"). |
'esriSpatialRelIntersects' |
return_geometry |
bool |
Whether to return the geometry (default is True). |
True |
outFields |
str |
The fields to be returned (default is "*"). |
'*' |
output |
str |
The output file path to save the GeoDataFrame (default is None). |
None |
**kwargs |
Any |
Additional keyword arguments to pass to the API. |
{} |
Returns:
Type | Description |
---|---|
gpd.GeoDataFrame |
The queried NWI data as a GeoDataFrame. |
Source code in leafmap/common.py
def get_nwi(
geometry: Dict[str, Any],
inSR: str = "4326",
outSR: str = "3857",
spatialRel: str = "esriSpatialRelIntersects",
return_geometry: bool = True,
outFields: str = "*",
output: Optional[str] = None,
**kwargs: Any,
) -> Union["gpd.GeoDataFrame", "pd.DataFrame", Dict[str, str]]:
"""
Query the NWI (National Wetlands Inventory) API using various geometry types.
https://fwspublicservices.wim.usgs.gov/wetlandsmapservice/rest/services/Wetlands/FeatureServer
Args:
geometry (dict): The geometry data (e.g., point, polygon, polyline, multipoint, etc.).
inSR (str): The input spatial reference (default is EPSG:4326).
outSR (str): The output spatial reference (default is EPSG:3857).
spatialRel (str): The spatial relationship (default is "esriSpatialRelIntersects").
return_geometry (bool): Whether to return the geometry (default is True).
outFields (str): The fields to be returned (default is "*").
output (str): The output file path to save the GeoDataFrame (default is None).
**kwargs: Additional keyword arguments to pass to the API.
Returns:
gpd.GeoDataFrame: The queried NWI data as a GeoDataFrame.
"""
import geopandas as gpd
import pandas as pd
from shapely.geometry import Polygon
def detect_geometry_type(geometry):
"""
Automatically detect the geometry type based on the structure of the geometry dictionary.
"""
if "x" in geometry and "y" in geometry:
return "esriGeometryPoint"
elif (
"xmin" in geometry
and "ymin" in geometry
and "xmax" in geometry
and "ymax" in geometry
):
return "esriGeometryEnvelope"
elif "rings" in geometry:
return "esriGeometryPolygon"
elif "paths" in geometry:
return "esriGeometryPolyline"
elif "points" in geometry:
return "esriGeometryMultipoint"
else:
raise ValueError("Unsupported geometry type or invalid geometry structure.")
# Convert GeoDataFrame to a dictionary if needed
if isinstance(geometry, gpd.GeoDataFrame):
geometry_dict = _convert_geodataframe_to_esri_format(geometry)[0]
geometry_type = detect_geometry_type(geometry_dict)
elif isinstance(geometry, dict):
geometry_type = detect_geometry_type(geometry)
geometry_dict = geometry
elif isinstance(geometry, str):
geometry_dict = geometry
else:
raise ValueError(
"Invalid geometry input. Must be a GeoDataFrame or a dictionary."
)
# Convert geometry to a JSON string (required by the API)
if isinstance(geometry_dict, dict):
geometry_json = json.dumps(geometry_dict)
else:
geometry_json = geometry_dict
# API URL for querying wetlands
url = "https://fwspublicservices.wim.usgs.gov/wetlandsmapservice/rest/services/Wetlands/MapServer/0/query"
# Construct the query parameters
params = {
"geometry": geometry_json, # The geometry as a JSON string
"geometryType": geometry_type, # Geometry type (automatically detected)
"inSR": inSR, # Spatial reference system (default is WGS84)
"spatialRel": spatialRel, # Spatial relationship (default is intersects)
"outFields": outFields, # Which fields to return (default is all fields)
"returnGeometry": str(
return_geometry
).lower(), # Whether to return the geometry
"f": "json", # Response format
}
for key, value in kwargs.items():
params[key] = value
# Make the GET request
response = requests.get(url, params=params)
# Check if the request was successful
if response.status_code == 200:
data = response.json() # Return the data as a Python dictionary
else:
return {"error": f"Request failed with status code {response.status_code}"}
# Extract the features
features = data["features"]
# Prepare the attribute data and geometries
attributes = [feature["attributes"] for feature in features]
# Create a DataFrame for attributes
df = pd.DataFrame(attributes)
df.rename(
columns={
"Shape__Length": "Shape_Length",
"Shape__Area": "Shape_Area",
"WETLAND_TYPE": "WETLAND_TY",
},
inplace=True,
)
if return_geometry:
geometries = [Polygon(feature["geometry"]["rings"][0]) for feature in features]
# Create a GeoDataFrame by combining the attributes and geometries
gdf = gpd.GeoDataFrame(
df,
geometry=geometries,
crs=f"EPSG:{data['spatialReference']['latestWkid']}",
)
if outSR != "3857":
gdf = gdf.to_crs(outSR)
if output is not None:
gdf.to_file(output)
return gdf
else:
return df
get_nwi_by_huc8(huc8=None, geometry=None, out_dir=None, quiet=True, layer='Wetlands', **kwargs)
¶
Fetches National Wetlands Inventory (NWI) data by HUC8 code.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
huc8 |
Optional[str] |
The HUC8 code to query the NWI data. It must be a string of length 8. |
None |
geometry |
Optional[Union[gpd.GeoDataFrame, str]] |
The geometry to derive the HUC8 code. It can be a GeoDataFrame or a file path. |
None |
out_dir |
Optional[str] |
The directory to save the downloaded data. Defaults to a temporary directory. |
None |
quiet |
bool |
Whether to suppress download progress messages. Defaults to True. |
True |
layer |
str |
The layer to fetch from the NWI data. It can be one of the following: Wetlands, Watershed, Riparian_Project_Metadata, Wetlands_Historic_Map_Info. Defaults to "Wetlands". |
'Wetlands' |
**kwargs |
Additional keyword arguments to pass to the download_file function. |
{} |
Returns:
Type | Description |
---|---|
gpd.GeoDataFrame |
The fetched NWI data as a GeoDataFrame. |
Exceptions:
Type | Description |
---|---|
ValueError |
If the HUC8 code is invalid or the layer is not allowed. |
Source code in leafmap/common.py
def get_nwi_by_huc8(
huc8: Optional[str] = None,
geometry: Optional[Union["gpd.GeoDataFrame", str]] = None,
out_dir: Optional[str] = None,
quiet: bool = True,
layer: str = "Wetlands",
**kwargs,
) -> "gpd.GeoDataFrame":
"""
Fetches National Wetlands Inventory (NWI) data by HUC8 code.
Args:
huc8 (Optional[str]): The HUC8 code to query the NWI data. It must be a
string of length 8.
geometry (Optional[Union[gpd.GeoDataFrame, str]]): The geometry to derive
the HUC8 code. It can be a GeoDataFrame or a file path.
out_dir (Optional[str]): The directory to save the downloaded data.
Defaults to a temporary directory.
quiet (bool): Whether to suppress download progress messages. Defaults to True.
layer (str): The layer to fetch from the NWI data. It can be one of the following:
Wetlands, Watershed, Riparian_Project_Metadata, Wetlands_Historic_Map_Info.
Defaults to "Wetlands".
**kwargs: Additional keyword arguments to pass to the download_file function.
Returns:
gpd.GeoDataFrame: The fetched NWI data as a GeoDataFrame.
Raises:
ValueError: If the HUC8 code is invalid or the layer is not allowed.
"""
import tempfile
import geopandas as gpd
if geometry is not None:
wbd = get_wbd(geometry, return_geometry=False)
huc8 = wbd["huc8"].values[0]
if isinstance(huc8, str) and len(huc8) == 8:
pass
else:
raise ValueError("Invalid HUC8 code. It must be a string of length 8.")
if out_dir is None:
out_dir = tempfile.gettempdir()
allowed_layers = [
"Wetlands",
"Watershed",
"Riparian_Project_Metadata",
"Wetlands_Historic_Map_Info",
"Wetlands_Project_Metadata",
]
if layer not in allowed_layers:
raise ValueError(f"Invalid layer. Allowed values are {allowed_layers}")
url = f"https://documentst.ecosphere.fws.gov/wetlands/downloads/watershed/HU8_{huc8}_Watershed.zip"
filename = os.path.join(out_dir, f"HU8_{huc8}_Watershed.zip")
download_file(url, filename, quiet=quiet, **kwargs)
data_dir = os.path.join(out_dir, f"HU8_{huc8}_Watershed")
filepath = os.path.join(data_dir, f"HU8_{huc8}_{layer}.shp")
gdf = gpd.read_file(filepath)
return gdf
get_overlap(img1, img2, overlap, out_img1=None, out_img2=None, to_cog=True)
¶
Get overlapping area of two images.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
img1 |
str |
Path to the first image. |
required |
img2 |
str |
Path to the second image. |
required |
overlap |
str |
Path to the output overlap area in GeoJSON format. |
required |
out_img1 |
str |
Path to the cropped image of the first image. |
None |
out_img2 |
str |
Path to the cropped image of the second image. |
None |
to_cog |
bool |
Whether to convert the output images to COG. |
True |
Returns:
Type | Description |
---|---|
str |
Path to the overlap area in GeoJSON format. |
Source code in leafmap/common.py
def get_overlap(img1, img2, overlap, out_img1=None, out_img2=None, to_cog=True):
"""Get overlapping area of two images.
Args:
img1 (str): Path to the first image.
img2 (str): Path to the second image.
overlap (str): Path to the output overlap area in GeoJSON format.
out_img1 (str, optional): Path to the cropped image of the first image.
out_img2 (str, optional): Path to the cropped image of the second image.
to_cog (bool, optional): Whether to convert the output images to COG.
Returns:
str: Path to the overlap area in GeoJSON format.
"""
import json
from osgeo import gdal, ogr, osr
import geopandas as gpd
extent = gdal.Info(img1, format="json")["wgs84Extent"]
poly1 = ogr.CreateGeometryFromJson(json.dumps(extent))
extent = gdal.Info(img2, format="json")["wgs84Extent"]
poly2 = ogr.CreateGeometryFromJson(json.dumps(extent))
intersection = poly1.Intersection(poly2)
gg = gdal.OpenEx(intersection.ExportToJson())
ds = gdal.VectorTranslate(
overlap,
srcDS=gg,
format="GeoJSON",
layerCreationOptions=["RFC7946=YES", "WRITE_BBOX=YES"],
)
ds = None
d = gdal.Open(img1)
proj = osr.SpatialReference(wkt=d.GetProjection())
epsg = proj.GetAttrValue("AUTHORITY", 1)
gdf = gpd.read_file(overlap)
gdf.to_crs(epsg=epsg, inplace=True)
gdf.to_file(overlap)
if out_img1 is not None:
clip_image(img1, overlap, out_img1, to_cog=to_cog)
if out_img2 is not None:
clip_image(img2, overlap, out_img2, to_cog=to_cog)
return overlap
get_overture_data(overture_type, bbox=None, columns=None, output=None)
¶
Fetches overture data and returns it as a GeoDataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
overture_type |
str |
The type of overture data to fetch.It can be one of the following: address|building|building_part|division|division_area|division_boundary|place| segment|connector|infrastructure|land|land_cover|land_use|water |
required |
bbox |
Tuple[float, float, float, float] |
The bounding box to filter the data. Defaults to None. |
None |
columns |
List[str] |
The columns to include in the output. Defaults to None. |
None |
output |
str |
The file path to save the output GeoDataFrame. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
gpd.GeoDataFrame |
The fetched overture data as a GeoDataFrame. |
Exceptions:
Type | Description |
---|---|
ImportError |
If the overture package is not installed. |
Source code in leafmap/common.py
def get_overture_data(
overture_type: str,
bbox: Tuple[float, float, float, float] = None,
columns: List[str] = None,
output: str = None,
) -> "gpd.GeoDataFrame":
"""Fetches overture data and returns it as a GeoDataFrame.
Args:
overture_type (str): The type of overture data to fetch.It can be one of the following:
address|building|building_part|division|division_area|division_boundary|place|
segment|connector|infrastructure|land|land_cover|land_use|water
bbox (Tuple[float, float, float, float], optional): The bounding box to
filter the data. Defaults to None.
columns (List[str], optional): The columns to include in the output.
Defaults to None.
output (str, optional): The file path to save the output GeoDataFrame.
Defaults to None.
Returns:
gpd.GeoDataFrame: The fetched overture data as a GeoDataFrame.
Raises:
ImportError: If the overture package is not installed.
"""
try:
from overturemaps import core
except ImportError:
install_package("overturemaps")
from overturemaps import core
gdf = core.geodataframe(overture_type, bbox=bbox)
if columns is not None:
gdf = gdf[columns]
gdf.crs = "EPSG:4326"
if output is not None:
gdf.to_file(output)
return gdf
get_palettable(types=None)
¶
Get a list of palettable color palettes.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
types |
list |
A list of palettable types to return, e.g., types=['matplotlib', 'cartocolors']. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
list |
A list of palettable color palettes. |
Source code in leafmap/common.py
def get_palettable(types=None):
"""Get a list of palettable color palettes.
Args:
types (list, optional): A list of palettable types to return, e.g., types=['matplotlib', 'cartocolors']. Defaults to None.
Returns:
list: A list of palettable color palettes.
"""
try:
import palettable
except ImportError:
raise ImportError(
"Please install the palettable package using 'pip install palettable'."
)
if types is not None and (not isinstance(types, list)):
raise ValueError("The types must be a list.")
allowed_palettes = [
"cartocolors",
"cmocean",
"colorbrewer",
"cubehelix",
"lightbartlein",
"matplotlib",
"mycarta",
"scientific",
"tableau",
"wesanderson",
]
if types is None:
types = allowed_palettes[:]
if all(x in allowed_palettes for x in types):
pass
else:
raise ValueError(
"The types must be one of the following: " + ", ".join(allowed_palettes)
)
palettes = []
if "cartocolors" in types:
cartocolors_diverging = [
f"cartocolors.diverging.{c}"
for c in dir(palettable.cartocolors.diverging)[:-19]
]
cartocolors_qualitative = [
f"cartocolors.qualitative.{c}"
for c in dir(palettable.cartocolors.qualitative)[:-19]
]
cartocolors_sequential = [
f"cartocolors.sequential.{c}"
for c in dir(palettable.cartocolors.sequential)[:-41]
]
palettes = (
palettes
+ cartocolors_diverging
+ cartocolors_qualitative
+ cartocolors_sequential
)
if "cmocean" in types:
cmocean_diverging = [
f"cmocean.diverging.{c}" for c in dir(palettable.cmocean.diverging)[:-19]
]
cmocean_sequential = [
f"cmocean.sequential.{c}" for c in dir(palettable.cmocean.sequential)[:-19]
]
palettes = palettes + cmocean_diverging + cmocean_sequential
if "colorbrewer" in types:
colorbrewer_diverging = [
f"colorbrewer.diverging.{c}"
for c in dir(palettable.colorbrewer.diverging)[:-19]
]
colorbrewer_qualitative = [
f"colorbrewer.qualitative.{c}"
for c in dir(palettable.colorbrewer.qualitative)[:-19]
]
colorbrewer_sequential = [
f"colorbrewer.sequential.{c}"
for c in dir(palettable.colorbrewer.sequential)[:-41]
]
palettes = (
palettes
+ colorbrewer_diverging
+ colorbrewer_qualitative
+ colorbrewer_sequential
)
if "cubehelix" in types:
cubehelix = [
"classic_16",
"cubehelix1_16",
"cubehelix2_16",
"cubehelix3_16",
"jim_special_16",
"perceptual_rainbow_16",
"purple_16",
"red_16",
]
cubehelix = [f"cubehelix.{c}" for c in cubehelix]
palettes = palettes + cubehelix
if "lightbartlein" in types:
lightbartlein_diverging = [
f"lightbartlein.diverging.{c}"
for c in dir(palettable.lightbartlein.diverging)[:-19]
]
lightbartlein_sequential = [
f"lightbartlein.sequential.{c}"
for c in dir(palettable.lightbartlein.sequential)[:-19]
]
palettes = palettes + lightbartlein_diverging + lightbartlein_sequential
if "matplotlib" in types:
matplotlib_colors = [
f"matplotlib.{c}" for c in dir(palettable.matplotlib)[:-16]
]
palettes = palettes + matplotlib_colors
if "mycarta" in types:
mycarta = [f"mycarta.{c}" for c in dir(palettable.mycarta)[:-16]]
palettes = palettes + mycarta
if "scientific" in types:
scientific_diverging = [
f"scientific.diverging.{c}"
for c in dir(palettable.scientific.diverging)[:-19]
]
scientific_sequential = [
f"scientific.sequential.{c}"
for c in dir(palettable.scientific.sequential)[:-19]
]
palettes = palettes + scientific_diverging + scientific_sequential
if "tableau" in types:
tableau = [f"tableau.{c}" for c in dir(palettable.tableau)[:-14]]
palettes = palettes + tableau
return palettes
get_palette_colors(cmap_name=None, n_class=None, hashtag=False)
¶
Get a palette from a matplotlib colormap. See the list of colormaps at https://matplotlib.org/stable/tutorials/colors/colormaps.html.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
cmap_name |
str |
The name of the matplotlib colormap. Defaults to None. |
None |
n_class |
int |
The number of colors. Defaults to None. |
None |
hashtag |
bool |
Whether to return a list of hex colors. Defaults to False. |
False |
Returns:
Type | Description |
---|---|
list |
A list of hex colors. |
Source code in leafmap/common.py
def get_palette_colors(cmap_name=None, n_class=None, hashtag=False):
"""Get a palette from a matplotlib colormap. See the list of colormaps at https://matplotlib.org/stable/tutorials/colors/colormaps.html.
Args:
cmap_name (str, optional): The name of the matplotlib colormap. Defaults to None.
n_class (int, optional): The number of colors. Defaults to None.
hashtag (bool, optional): Whether to return a list of hex colors. Defaults to False.
Returns:
list: A list of hex colors.
"""
import matplotlib as mpl
import matplotlib.pyplot as plt
try:
cmap = plt.get_cmap(cmap_name, n_class)
except:
cmap = plt.cm.get_cmap(cmap_name, n_class)
colors = [mpl.colors.rgb2hex(cmap(i))[1:] for i in range(cmap.N)]
if hashtag:
colors = ["#" + i for i in colors]
return colors
get_solar_data(lat, lon, radiusMeters=50, view='FULL_LAYERS', requiredQuality='HIGH', pixelSizeMeters=0.1, api_key=None, header=None, out_dir=None, basename=None, quiet=False, **kwargs)
¶
Retrieve solar data for a specific location from Google's Solar API https://developers.google.com/maps/documentation/solar. You need to enable Solar API from https://console.cloud.google.com/google/maps-apis/api-list.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
lat |
float |
Latitude of the location. |
required |
lon |
float |
Longitude of the location. |
required |
radiusMeters |
int |
Radius in meters for the data retrieval (default is 50). |
50 |
view |
str |
View type (default is "FULL_LAYERS"). For more options, see https://bit.ly/3LazuBi. |
'FULL_LAYERS' |
requiredQuality |
str |
Required quality level (default is "HIGH"). |
'HIGH' |
pixelSizeMeters |
float |
Pixel size in meters (default is 0.1). |
0.1 |
api_key |
str |
Google API key for authentication (if not provided, checks 'GOOGLE_API_KEY' environment variable). |
None |
header |
dict |
Additional HTTP headers to include in the request. |
None |
out_dir |
str |
Directory where downloaded files will be saved. |
None |
basename |
str |
Base name for the downloaded files (default is generated from imagery date). |
None |
quiet |
bool |
If True, suppress progress messages during file downloads (default is False). |
False |
**kwargs |
Any |
Additional keyword arguments to be passed to the download_file function. |
{} |
Returns:
Type | Description |
---|---|
Dict[str, str] |
A dictionary mapping file names to their corresponding paths. |
Source code in leafmap/common.py
def get_solar_data(
lat: float,
lon: float,
radiusMeters: int = 50,
view: str = "FULL_LAYERS",
requiredQuality: str = "HIGH",
pixelSizeMeters: float = 0.1,
api_key: Optional[str] = None,
header: Optional[Dict[str, str]] = None,
out_dir: Optional[str] = None,
basename: Optional[str] = None,
quiet: bool = False,
**kwargs: Any,
) -> Dict[str, str]:
"""
Retrieve solar data for a specific location from Google's Solar API https://developers.google.com/maps/documentation/solar.
You need to enable Solar API from https://console.cloud.google.com/google/maps-apis/api-list.
Args:
lat (float): Latitude of the location.
lon (float): Longitude of the location.
radiusMeters (int, optional): Radius in meters for the data retrieval (default is 50).
view (str, optional): View type (default is "FULL_LAYERS"). For more options, see https://bit.ly/3LazuBi.
requiredQuality (str, optional): Required quality level (default is "HIGH").
pixelSizeMeters (float, optional): Pixel size in meters (default is 0.1).
api_key (str, optional): Google API key for authentication (if not provided, checks 'GOOGLE_API_KEY' environment variable).
header (dict, optional): Additional HTTP headers to include in the request.
out_dir (str, optional): Directory where downloaded files will be saved.
basename (str, optional): Base name for the downloaded files (default is generated from imagery date).
quiet (bool, optional): If True, suppress progress messages during file downloads (default is False).
**kwargs: Additional keyword arguments to be passed to the download_file function.
Returns:
Dict[str, str]: A dictionary mapping file names to their corresponding paths.
"""
if api_key is None:
api_key = os.environ.get("GOOGLE_API_KEY", "")
if api_key == "":
raise ValueError("GOOGLE_API_KEY is required to use this function.")
url = "https://solar.googleapis.com/v1/dataLayers:get"
params = {
"location.latitude": lat,
"location.longitude": lon,
"radiusMeters": radiusMeters,
"view": view,
"requiredQuality": requiredQuality,
"pixelSizeMeters": pixelSizeMeters,
"key": api_key,
}
solar_data = requests.get(url, params=params, headers=header).json()
links = {}
for key in solar_data.keys():
if "Url" in key:
if isinstance(solar_data[key], list):
urls = [url + "&key=" + api_key for url in solar_data[key]]
links[key] = urls
else:
links[key] = solar_data[key] + "&key=" + api_key
if basename is None:
date = solar_data["imageryDate"]
year = date["year"]
month = date["month"]
day = date["day"]
basename = f"{year}_{str(month).zfill(2)}_{str(day).zfill(2)}"
filenames = {}
for link in links:
if isinstance(links[link], list):
for i, url in enumerate(links[link]):
filename = (
f"{basename}_{link.replace('Urls', '')}_{str(i+1).zfill(2)}.tif"
)
if out_dir is not None:
filename = os.path.join(out_dir, filename)
download_file(url, filename, quiet=quiet, **kwargs)
filenames[link.replace("Urls", "") + "_" + str(i).zfill(2)] = filename
else:
name = link.replace("Url", "")
filename = f"{basename}_{name}.tif"
if out_dir is not None:
filename = os.path.join(out_dir, filename)
download_file(links[link], filename, quiet=quiet, **kwargs)
filenames[name] = filename
return filenames
get_stac_collections(url, **kwargs)
¶
Retrieve a list of STAC collections from a URL. This function is adapted from https://github.com/mykolakozyr/stacdiscovery/blob/a5d1029aec9c428a7ce7ae615621ea8915162824/app.py#L31. Credits to Mykola Kozyr.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
A URL to a STAC catalog. |
required |
**kwargs |
Additional keyword arguments to pass to the pystac Client.open() method. See https://pystac-client.readthedocs.io/en/stable/api.html#pystac_client.Client.open |
{} |
Returns:
Type | Description |
---|---|
list |
A list of STAC collections. |
Source code in leafmap/common.py
def get_stac_collections(url, **kwargs):
"""Retrieve a list of STAC collections from a URL.
This function is adapted from https://github.com/mykolakozyr/stacdiscovery/blob/a5d1029aec9c428a7ce7ae615621ea8915162824/app.py#L31.
Credits to Mykola Kozyr.
Args:
url (str): A URL to a STAC catalog.
**kwargs: Additional keyword arguments to pass to the pystac Client.open() method.
See https://pystac-client.readthedocs.io/en/stable/api.html#pystac_client.Client.open
Returns:
list: A list of STAC collections.
"""
from pystac_client import Client
# Expensive function. Added cache for it.
# Empty list that would be used for a dataframe to collect and visualize info about collections
root_catalog = Client.open(url, **kwargs)
collections_list = []
# Reading collections in the Catalog
collections = list(root_catalog.get_collections())
print(collections)
for collection in collections:
id = collection.id
title = collection.title
# bbox = collection.extent.spatial.bboxes # not in use for the first release
# interval = collection.extent.temporal.intervals # not in use for the first release
description = collection.description
# creating a list of lists of values
collections_list.append([id, title, description])
return collections_list
get_stac_items(url, collection, limit=None, bbox=None, datetime=None, intersects=None, ids=None, open_args=None, **kwargs)
¶
Retrieve a list of STAC items from a URL and a collection. This function is adapted from https://github.com/mykolakozyr/stacdiscovery/blob/a5d1029aec9c428a7ce7ae615621ea8915162824/app.py#L49. Credits to Mykola Kozyr. Available parameters can be found at https://github.com/radiantearth/stac-api-spec/tree/master/item-search
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
A URL to a STAC catalog. |
required |
collection |
str |
A STAC collection ID. |
required |
limit |
int |
The maximum number of results to return (page size). Defaults to None. |
None |
bbox |
tuple |
Requested bounding box in the format of (minx, miny, maxx, maxy). Defaults to None. |
None |
datetime |
str |
Single date+time, or a range ('/' separator), formatted to RFC 3339, section 5.6. Use double dots .. for open date ranges. |
None |
intersects |
dict |
A dictionary representing a GeoJSON Geometry. Searches items by performing intersection between their geometry and provided GeoJSON geometry. All GeoJSON geometry types must be supported. |
None |
ids |
list |
A list of item ids to return. |
None |
open_args |
dict |
A dictionary of arguments to pass to the pystac Client.open() method. Defaults to None. |
None |
**kwargs |
Additional keyword arguments to pass to the Catalog.search() method. |
{} |
Returns:
Type | Description |
---|---|
GeoPandas.GeoDataFraem |
A GeoDataFrame with the STAC items. |
Source code in leafmap/common.py
def get_stac_items(
url,
collection,
limit=None,
bbox=None,
datetime=None,
intersects=None,
ids=None,
open_args=None,
**kwargs,
):
"""Retrieve a list of STAC items from a URL and a collection.
This function is adapted from https://github.com/mykolakozyr/stacdiscovery/blob/a5d1029aec9c428a7ce7ae615621ea8915162824/app.py#L49.
Credits to Mykola Kozyr.
Available parameters can be found at https://github.com/radiantearth/stac-api-spec/tree/master/item-search
Args:
url (str): A URL to a STAC catalog.
collection (str): A STAC collection ID.
limit (int, optional): The maximum number of results to return (page size). Defaults to None.
bbox (tuple, optional): Requested bounding box in the format of (minx, miny, maxx, maxy). Defaults to None.
datetime (str, optional): Single date+time, or a range ('/' separator), formatted to RFC 3339, section 5.6. Use double dots .. for open date ranges.
intersects (dict, optional): A dictionary representing a GeoJSON Geometry. Searches items by performing intersection between their geometry and provided GeoJSON geometry. All GeoJSON geometry types must be supported.
ids (list, optional): A list of item ids to return.
open_args (dict, optional): A dictionary of arguments to pass to the pystac Client.open() method. Defaults to None.
**kwargs: Additional keyword arguments to pass to the Catalog.search() method.
Returns:
GeoPandas.GeoDataFraem: A GeoDataFrame with the STAC items.
"""
import itertools
import geopandas as gpd
from shapely.geometry import shape
from pystac_client import Client
# Empty list that would be used for a dataframe to collect and visualize info about collections
items_list = []
if open_args is None:
open_args = {}
root_catalog = Client.open(url)
if limit:
kwargs["limit"] = limit
if bbox:
kwargs["bbox"] = bbox
if datetime:
kwargs["datetime"] = datetime
if intersects:
kwargs["intersects"] = intersects
if ids:
kwargs["ids"] = ids
if kwargs:
try:
catalog = root_catalog.search(collections=collection, **kwargs)
except NotImplementedError:
catalog = root_catalog
else:
catalog = root_catalog
iterable = catalog.get_all_items()
items = list(
itertools.islice(iterable, limit)
) # getting first 25000 items. To Do some smarter logic
if len(items) == 0:
try:
catalog = root_catalog.get_child(collection)
iterable = catalog.get_all_items()
items = list(itertools.islice(iterable, limit))
except Exception as _:
print("Ooops, it looks like this collection does not have items.")
return None
# Iterating over items to collect main information
for item in items:
id = item.id
geometry = shape(item.geometry)
datetime = (
item.datetime
or item.properties["datetime"]
or item.properties["end_datetime"]
or item.properties["start_datetime"]
)
links = item.links
for link in links:
if link.rel == "self":
self_url = link.target
assets_list = []
assets = item.assets
for asset in assets:
assets_list.append(asset)
# creating a list of lists of values
items_list.append([id, geometry, datetime, self_url, assets_list])
if limit is not None:
items_list = items_list[:limit]
items_df = gpd.GeoDataFrame(items_list)
items_df.columns = ["id", "geometry", "datetime", "self_url", "assets_list"]
items_gdf = items_df.set_geometry("geometry")
items_gdf["datetime"] = items_gdf["datetime"].astype(
str
) # specifically for KeplerGL. See https://github.com/keplergl/kepler.gl/issues/602
# items_gdf["assets_list"] = items_gdf["assets_list"].astype(str) #specifically for KeplerGL. See https://github.com/keplergl/kepler.gl/issues/602
items_gdf.set_crs(epsg=4326, inplace=True)
return items_gdf
get_wbd(geometry=None, searchText=None, inSR='4326', outSR='3857', digit=8, spatialRel='esriSpatialRelIntersects', return_geometry=True, outFields='*', output=None, **kwargs)
¶
Query the WBD (Watershed Boundary Dataset) API using various geometry types or a GeoDataFrame. https://hydro.nationalmap.gov/arcgis/rest/services/wbd/MapServer
Parameters:
Name | Type | Description | Default |
---|---|---|---|
geometry |
Union[gpd.GeoDataFrame, Dict] |
The geometry data (GeoDataFrame or geometry dict). |
None |
inSR |
str |
The input spatial reference (default is EPSG:4326). |
'4326' |
outSR |
str |
The output spatial reference (default is EPSG:3857). |
'3857' |
digit |
int |
The digit code for the WBD layer (default is 8). |
8 |
spatialRel |
str |
The spatial relationship (default is "esriSpatialRelIntersects"). |
'esriSpatialRelIntersects' |
return_geometry |
bool |
Whether to return the geometry (default is True). |
True |
outFields |
str |
The fields to be returned (default is "*"). |
'*' |
output |
Optional[str] |
The output file path to save the GeoDataFrame (default is None). |
None |
**kwargs |
Any |
Additional keyword arguments to pass to the API. |
{} |
Returns:
Type | Description |
---|---|
gpd.GeoDataFrame or pd.DataFrame |
The queried WBD data as a GeoDataFrame or DataFrame. |
Source code in leafmap/common.py
def get_wbd(
geometry: Union["gpd.GeoDataFrame", Dict[str, Any]] = None,
searchText: Optional[str] = None,
inSR: str = "4326",
outSR: str = "3857",
digit: int = 8,
spatialRel: str = "esriSpatialRelIntersects",
return_geometry: bool = True,
outFields: str = "*",
output: Optional[str] = None,
**kwargs: Any,
) -> Union["gpd.GeoDataFrame", "pd.DataFrame", Dict[str, str]]:
"""
Query the WBD (Watershed Boundary Dataset) API using various geometry types or a GeoDataFrame.
https://hydro.nationalmap.gov/arcgis/rest/services/wbd/MapServer
Args:
geometry (Union[gpd.GeoDataFrame, Dict]): The geometry data (GeoDataFrame or geometry dict).
inSR (str): The input spatial reference (default is EPSG:4326).
outSR (str): The output spatial reference (default is EPSG:3857).
digit (int): The digit code for the WBD layer (default is 8).
spatialRel (str): The spatial relationship (default is "esriSpatialRelIntersects").
return_geometry (bool): Whether to return the geometry (default is True).
outFields (str): The fields to be returned (default is "*").
output (Optional[str]): The output file path to save the GeoDataFrame (default is None).
**kwargs: Additional keyword arguments to pass to the API.
Returns:
gpd.GeoDataFrame or pd.DataFrame: The queried WBD data as a GeoDataFrame or DataFrame.
"""
import geopandas as gpd
import pandas as pd
from shapely.geometry import Polygon
def detect_geometry_type(geometry):
"""
Automatically detect the geometry type based on the structure of the geometry dictionary.
"""
if "x" in geometry and "y" in geometry:
return "esriGeometryPoint"
elif (
"xmin" in geometry
and "ymin" in geometry
and "xmax" in geometry
and "ymax" in geometry
):
return "esriGeometryEnvelope"
elif "rings" in geometry:
return "esriGeometryPolygon"
elif "paths" in geometry:
return "esriGeometryPolyline"
elif "points" in geometry:
return "esriGeometryMultipoint"
else:
raise ValueError("Unsupported geometry type or invalid geometry structure.")
allowed_digit_values = [2, 4, 6, 8, 10, 12, 14, 16]
if digit not in allowed_digit_values:
raise ValueError(
f"Invalid digit value. Allowed values are {allowed_digit_values}"
)
layer = allowed_digit_values.index(digit) + 1
# Convert GeoDataFrame to a dictionary if needed
if isinstance(geometry, gpd.GeoDataFrame):
geometry_dict = _convert_geodataframe_to_esri_format(geometry)[0]
geometry_type = detect_geometry_type(geometry_dict)
elif isinstance(geometry, dict):
geometry_type = detect_geometry_type(geometry)
geometry_dict = geometry
elif isinstance(geometry, str):
geometry_dict = geometry
elif searchText is None:
raise ValueError(
"Invalid geometry input. Must be a GeoDataFrame or a dictionary."
)
else:
geometry_dict = None
if geometry_dict is not None:
# Convert geometry to a JSON string (required by the API)
if isinstance(geometry_dict, dict):
geometry_json = json.dumps(geometry_dict)
else:
geometry_json = geometry_dict
# Construct the query parameters
params = {
"geometry": geometry_json,
"geometryType": geometry_type,
"inSR": inSR,
"spatialRel": spatialRel,
"outFields": outFields,
"returnGeometry": str(return_geometry).lower(),
"f": "json",
}
# API URL for querying the WBD
url = f"https://hydro.nationalmap.gov/arcgis/rest/services/wbd/MapServer/{layer}/query"
else:
# Construct the query parameters
params = {
"searchText": searchText,
"contains": "true",
"layers": str(layer),
"inSR": inSR,
"outFields": outFields,
"returnGeometry": str(return_geometry).lower(),
"f": "json",
}
url = f"https://hydro.nationalmap.gov/arcgis/rest/services/wbd/MapServer/find"
# Add additional keyword arguments
for key, value in kwargs.items():
params[key] = value
# Make the GET request
response = requests.get(url, params=params)
if response.status_code != 200:
return {"error": f"Request failed with status code {response.status_code}"}
data = response.json()
if geometry_dict is not None:
# Extract features from the API response
features = data.get("features", [])
crs = f"EPSG:{data['spatialReference']['latestWkid']}"
else:
features = data.get("results", [])
crs = f"EPSG:{data['results'][0]['geometry']['spatialReference']['latestWkid']}"
# Prepare attribute data and geometries
attributes = [feature["attributes"] for feature in features]
df = pd.DataFrame(attributes)
df.rename(
columns={"Shape__Length": "Shape_Length", "Shape__Area": "Shape_Area"},
inplace=True,
)
# Handle geometries
if return_geometry:
geometries = [
(
Polygon(feature["geometry"]["rings"][0])
if "rings" in feature["geometry"]
else None
)
for feature in features
]
gdf = gpd.GeoDataFrame(
df,
geometry=geometries,
crs=crs,
)
if outSR != "3857":
gdf = gdf.to_crs(outSR)
if output is not None:
gdf.to_file(output)
return gdf
else:
return df
get_wms_layers(url)
¶
Returns a list of WMS layers from a WMS service.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
The URL of the WMS service. |
required |
Returns:
Type | Description |
---|---|
list |
A list of WMS layers. |
Source code in leafmap/common.py
def get_wms_layers(url):
"""Returns a list of WMS layers from a WMS service.
Args:
url (str): The URL of the WMS service.
Returns:
list: A list of WMS layers.
"""
try:
from owslib.wms import WebMapService
except ImportError:
raise ImportError("Please install owslib using 'pip install owslib'.")
wms = WebMapService(url)
layers = list(wms.contents)
layers.sort()
return layers
gif_fading(in_gif, out_gif, duration=1, verbose=True)
¶
Fade in/out the gif.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_gif |
str |
The input gif file. Can be a directory path or http URL, e.g., "https://i.imgur.com/ZWSZC5z.gif" |
required |
out_gif |
str |
The output gif file. |
required |
duration |
float |
The duration of the fading. Defaults to 1. |
1 |
verbose |
bool |
Whether to print the progress. Defaults to True. |
True |
Exceptions:
Type | Description |
---|---|
FileNotFoundError |
Raise exception when the input gif does not exist. |
Exception |
Raise exception when ffmpeg is not installed. |
Source code in leafmap/common.py
def gif_fading(in_gif, out_gif, duration=1, verbose=True):
"""Fade in/out the gif.
Args:
in_gif (str): The input gif file. Can be a directory path or http URL, e.g., "https://i.imgur.com/ZWSZC5z.gif"
out_gif (str): The output gif file.
duration (float, optional): The duration of the fading. Defaults to 1.
verbose (bool, optional): Whether to print the progress. Defaults to True.
Raises:
FileNotFoundError: Raise exception when the input gif does not exist.
Exception: Raise exception when ffmpeg is not installed.
"""
import glob
import tempfile
current_dir = os.getcwd()
if isinstance(in_gif, str) and in_gif.startswith("http"):
ext = os.path.splitext(in_gif)[1]
file_path = temp_file_path(ext)
download_from_url(in_gif, file_path, verbose=verbose)
in_gif = file_path
in_gif = os.path.abspath(in_gif)
if not in_gif.endswith(".gif"):
raise Exception("in_gif must be a gif file.")
if " " in in_gif:
raise Exception("The filename cannot contain spaces.")
out_gif = os.path.abspath(out_gif)
if not os.path.exists(os.path.dirname(out_gif)):
os.makedirs(os.path.dirname(out_gif))
if not os.path.exists(in_gif):
raise FileNotFoundError(f"{in_gif} does not exist.")
basename = os.path.basename(in_gif).replace(".gif", "")
temp_dir = os.path.join(tempfile.gettempdir(), basename)
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
gif_to_png(in_gif, temp_dir, verbose=verbose)
os.chdir(temp_dir)
images = list(glob.glob(os.path.join(temp_dir, "*.png")))
count = len(images)
files = []
for i in range(1, count + 1):
files.append(f"-loop 1 -t {duration} -i {i}.png")
inputs = " ".join(files)
filters = []
for i in range(1, count):
if i == 1:
filters.append(
f"\"[1:v][0:v]blend=all_expr='A*(if(gte(T,3),1,T/3))+B*(1-(if(gte(T,3),1,T/3)))'[v0];"
)
else:
filters.append(
f"[{i}:v][{i-1}:v]blend=all_expr='A*(if(gte(T,3),1,T/3))+B*(1-(if(gte(T,3),1,T/3)))'[v{i-1}];"
)
last_filter = ""
for i in range(count - 1):
last_filter += f"[v{i}]"
last_filter += f'concat=n={count-1}:v=1:a=0[v]" -map "[v]"'
filters.append(last_filter)
filters = " ".join(filters)
cmd = f"ffmpeg -y -loglevel error {inputs} -filter_complex {filters} {out_gif}"
os.system(cmd)
try:
shutil.rmtree(temp_dir)
except Exception as e:
print(e)
os.chdir(current_dir)
gif_to_mp4(in_gif, out_mp4)
¶
Converts a gif to mp4.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_gif |
str |
The input gif file. |
required |
out_mp4 |
str |
The output mp4 file. |
required |
Source code in leafmap/common.py
def gif_to_mp4(in_gif, out_mp4):
"""Converts a gif to mp4.
Args:
in_gif (str): The input gif file.
out_mp4 (str): The output mp4 file.
"""
from PIL import Image
if not os.path.exists(in_gif):
raise FileNotFoundError(f"{in_gif} does not exist.")
out_mp4 = os.path.abspath(out_mp4)
if not out_mp4.endswith(".mp4"):
out_mp4 = out_mp4 + ".mp4"
if not os.path.exists(os.path.dirname(out_mp4)):
os.makedirs(os.path.dirname(out_mp4))
if not is_tool("ffmpeg"):
print("ffmpeg is not installed on your computer.")
return
width, height = Image.open(in_gif).size
if width % 2 == 0 and height % 2 == 0:
cmd = f"ffmpeg -loglevel error -i {in_gif} -vcodec libx264 -crf 25 -pix_fmt yuv420p {out_mp4}"
os.system(cmd)
else:
width += width % 2
height += height % 2
cmd = f"ffmpeg -loglevel error -i {in_gif} -vf scale={width}:{height} -vcodec libx264 -crf 25 -pix_fmt yuv420p {out_mp4}"
os.system(cmd)
if not os.path.exists(out_mp4):
raise Exception(f"Failed to create mp4 file.")
gif_to_png(in_gif, out_dir=None, prefix='', verbose=True)
¶
Converts a gif to png.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_gif |
str |
The input gif file. |
required |
out_dir |
str |
The output directory. Defaults to None. |
None |
prefix |
str |
The prefix of the output png files. Defaults to None. |
'' |
verbose |
bool |
Whether to print the progress. Defaults to True. |
True |
Exceptions:
Type | Description |
---|---|
FileNotFoundError |
Raise exception when the input gif does not exist. |
Exception |
Raise exception when ffmpeg is not installed. |
Source code in leafmap/common.py
def gif_to_png(in_gif, out_dir=None, prefix="", verbose=True):
"""Converts a gif to png.
Args:
in_gif (str): The input gif file.
out_dir (str, optional): The output directory. Defaults to None.
prefix (str, optional): The prefix of the output png files. Defaults to None.
verbose (bool, optional): Whether to print the progress. Defaults to True.
Raises:
FileNotFoundError: Raise exception when the input gif does not exist.
Exception: Raise exception when ffmpeg is not installed.
"""
import tempfile
in_gif = os.path.abspath(in_gif)
if " " in in_gif:
raise Exception("in_gif cannot contain spaces.")
if not os.path.exists(in_gif):
raise FileNotFoundError(f"{in_gif} does not exist.")
basename = os.path.basename(in_gif).replace(".gif", "")
if out_dir is None:
out_dir = os.path.join(tempfile.gettempdir(), basename)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
elif isinstance(out_dir, str) and not os.path.exists(out_dir):
os.makedirs(out_dir)
elif not isinstance(out_dir, str):
raise Exception("out_dir must be a string.")
out_dir = os.path.abspath(out_dir)
cmd = f"ffmpeg -loglevel error -i {in_gif} -vsync 0 {out_dir}/{prefix}%d.png"
os.system(cmd)
if verbose:
print(f"Images are saved to {out_dir}")
github_delete_asset(username, repository, asset_id, access_token=None)
¶
Deletes an asset from a GitHub release.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
username |
str |
GitHub username or organization name. |
required |
repository |
str |
Name of the GitHub repository. |
required |
asset_id |
int |
ID of the asset to delete. |
required |
access_token |
str |
Personal access token for authentication. |
None |
Source code in leafmap/common.py
def github_delete_asset(username, repository, asset_id, access_token=None):
"""
Deletes an asset from a GitHub release.
Args:
username (str): GitHub username or organization name.
repository (str): Name of the GitHub repository.
asset_id (int): ID of the asset to delete.
access_token (str): Personal access token for authentication.
"""
if access_token is None:
access_token = get_api_key("GITHUB_API_TOKEN")
url = f"https://api.github.com/repos/{username}/{repository}/releases/assets/{asset_id}"
headers = {
"Authorization": f"token {access_token}",
"Accept": "application/vnd.github.v3+json",
}
response = requests.delete(url, headers=headers)
if response.status_code == 204:
print(f"Successfully deleted asset ID: {asset_id}")
else:
print(f"Error: Unable to delete asset (Status code: {response.status_code})")
github_get_release_assets(username, repository, release_id, access_token=None)
¶
Fetches the assets for a given release.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
username |
str |
GitHub username or organization name. |
required |
repository |
str |
Name of the GitHub repository. |
required |
release_id |
int |
ID of the release to fetch assets for. |
required |
access_token |
str |
Personal access token for authentication. |
None |
Returns:
Type | Description |
---|---|
list |
List of assets if successful, None otherwise. |
Source code in leafmap/common.py
def github_get_release_assets(username, repository, release_id, access_token=None):
"""
Fetches the assets for a given release.
Args:
username (str): GitHub username or organization name.
repository (str): Name of the GitHub repository.
release_id (int): ID of the release to fetch assets for.
access_token (str): Personal access token for authentication.
Returns:
list: List of assets if successful, None otherwise.
"""
if access_token is None:
access_token = get_api_key("GITHUB_API_TOKEN")
url = f"https://api.github.com/repos/{username}/{repository}/releases/{release_id}/assets"
headers = {
"Authorization": f"token {access_token}",
"Accept": "application/vnd.github.v3+json",
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
else:
print(f"Error: Unable to fetch assets (Status code: {response.status_code})")
return None
github_get_release_id_by_tag(username, repository, tag_name, access_token=None)
¶
Fetches the release ID by tag name for a given GitHub repository.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
username |
str |
GitHub username or organization name. |
required |
repository |
str |
Name of the GitHub repository. |
required |
tag_name |
str |
Tag name of the release. |
required |
access_token |
str |
Personal access token for authentication. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
int |
The release ID if found, None otherwise. |
Source code in leafmap/common.py
def github_get_release_id_by_tag(username, repository, tag_name, access_token=None):
"""
Fetches the release ID by tag name for a given GitHub repository.
Args:
username (str): GitHub username or organization name.
repository (str): Name of the GitHub repository.
tag_name (str): Tag name of the release.
access_token (str, optional): Personal access token for authentication. Defaults to None.
Returns:
int: The release ID if found, None otherwise.
"""
if access_token is None:
access_token = get_api_key("GITHUB_API_TOKEN")
# GitHub API URL for fetching releases
url = (
f"https://api.github.com/repos/{username}/{repository}/releases/tags/{tag_name}"
)
# Headers for authentication (optional)
headers = {"Authorization": f"token {access_token}"} if access_token else {}
# Make the request to the GitHub API
response = requests.get(url, headers=headers)
# Check if the request was successful
if response.status_code == 200:
release_info = response.json()
return release_info.get("id")
else:
print(
f"Error: Unable to fetch release info for tag {tag_name} (Status code: {response.status_code})"
)
return None
github_raw_url(url)
¶
Get the raw URL for a GitHub file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
The GitHub URL. |
required |
Returns:
Type | Description |
---|---|
str |
The raw URL. |
Source code in leafmap/common.py
def github_raw_url(url):
"""Get the raw URL for a GitHub file.
Args:
url (str): The GitHub URL.
Returns:
str: The raw URL.
"""
if isinstance(url, str) and url.startswith("https://github.com/") and "blob" in url:
url = url.replace("github.com", "raw.githubusercontent.com").replace(
"blob/", ""
)
return url
github_upload_asset_to_release(username, repository, release_id, asset_path, quiet=False, access_token=None)
¶
Uploads an asset to a GitHub release.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
username |
str |
GitHub username or organization name. |
required |
repository |
str |
Name of the GitHub repository. |
required |
release_id |
int |
ID of the release to upload the asset to. |
required |
asset_path |
str |
Path to the asset file. |
required |
access_token |
str |
Personal access token for authentication. |
None |
Returns:
Type | Description |
---|---|
dict |
The response JSON from the GitHub API if the upload is successful. None: If the upload fails. |
Source code in leafmap/common.py
def github_upload_asset_to_release(
username, repository, release_id, asset_path, quiet=False, access_token=None
):
"""
Uploads an asset to a GitHub release.
Args:
username (str): GitHub username or organization name.
repository (str): Name of the GitHub repository.
release_id (int): ID of the release to upload the asset to.
asset_path (str): Path to the asset file.
access_token (str): Personal access token for authentication.
Returns:
dict: The response JSON from the GitHub API if the upload is successful.
None: If the upload fails.
"""
if access_token is None:
access_token = get_api_key("GITHUB_API_TOKEN")
# GitHub API URL for uploading release assets
url = f"https://uploads.github.com/repos/{username}/{repository}/releases/{release_id}/assets"
# Extract the filename from the asset path
asset_name = os.path.basename(asset_path)
# Set the headers for the upload request
headers = {
"Authorization": f"token {access_token}",
"Content-Type": "application/octet-stream",
}
# Set the parameters for the upload request
params = {"name": asset_name}
# Check if the asset already exists
assets = github_get_release_assets(username, repository, release_id, access_token)
if assets:
for asset in assets:
if asset["name"] == asset_name:
github_delete_asset(username, repository, asset["id"], access_token)
break
# Open the asset file in binary mode
with open(asset_path, "rb") as asset_file:
# Make the request to upload the asset
response = requests.post(url, headers=headers, params=params, data=asset_file)
# Check if the request was successful
if response.status_code == 201:
print(f"Successfully uploaded asset: {asset_name}")
if not quiet:
return response.json()
else:
return None
else:
print(f"Error: Unable to upload asset (Status code: {response.status_code})")
if not quiet:
print(response.json())
return None
google_buildings_csv_to_vector(filename, output=None, **kwargs)
¶
Convert a CSV file containing Google Buildings data to a GeoJSON vector file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filename |
str |
The path to the input CSV file. |
required |
output |
str |
The path to the output GeoJSON file. If not provided, the output file will have the same name as the input file with the extension changed to '.geojson'. |
None |
**kwargs |
Additional keyword arguments that are passed to the |
{} |
Returns:
Type | Description |
---|---|
None |
None |
Source code in leafmap/common.py
def google_buildings_csv_to_vector(
filename: str, output: Optional[str] = None, **kwargs
) -> None:
"""
Convert a CSV file containing Google Buildings data to a GeoJSON vector file.
Args:
filename (str): The path to the input CSV file.
output (str, optional): The path to the output GeoJSON file. If not provided, the output file will have the same
name as the input file with the extension changed to '.geojson'.
**kwargs: Additional keyword arguments that are passed to the `to_file` method of the GeoDataFrame.
Returns:
None
"""
import pandas as pd
import geopandas as gpd
from shapely import wkt
df = pd.read_csv(filename)
# Create a geometry column from the "geometry" column in the DataFrame
df["geometry"] = df["geometry"].apply(wkt.loads)
# Convert the pandas DataFrame to a GeoDataFrame
gdf = gpd.GeoDataFrame(df, geometry="geometry")
gdf.crs = "EPSG:4326"
if output is None:
output = os.path.splitext(filename)[0] + ".geojson"
gdf.to_file(output, **kwargs)
h5_keys(filename)
¶
Retrieve the keys (dataset names) within an HDF5 file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filename |
str |
The filename of the HDF5 file. |
required |
Returns:
Type | Description |
---|---|
List[str] |
A list of dataset names present in the HDF5 file. |
Exceptions:
Type | Description |
---|---|
ImportError |
Raised if h5py is not installed. |
Examples:
>>> keys = h5_keys('data.h5')
>>> print(keys)
[
Source code in leafmap/common.py
def h5_keys(filename: str) -> List[str]:
"""
Retrieve the keys (dataset names) within an HDF5 file.
Args:
filename (str): The filename of the HDF5 file.
Returns:
List[str]: A list of dataset names present in the HDF5 file.
Raises:
ImportError: Raised if h5py is not installed.
Example:
>>> keys = h5_keys('data.h5')
>>> print(keys)
[
"""
try:
import h5py
except ImportError:
raise ImportError(
"h5py must be installed to use this function. Please install it with 'pip install h5py'."
)
with h5py.File(filename, "r") as f:
keys = list(f.keys())
return keys
h5_to_gdf(filenames, dataset, lat='lat_lowestmode', lon='lon_lowestmode', columns=None, crs='EPSG:4326', nodata=None, **kwargs)
¶
Read data from one or multiple HDF5 files and return as a GeoDataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filenames |
str or List[str] |
The filename(s) of the HDF5 file(s). |
required |
dataset |
str |
The dataset name within the H5 file(s). |
required |
lat |
str |
The column name representing latitude. Default is 'lat_lowestmode'. |
'lat_lowestmode' |
lon |
str |
The column name representing longitude. Default is 'lon_lowestmode'. |
'lon_lowestmode' |
columns |
List[str] |
List of column names to include. If None, all columns will be included. Default is None. |
None |
crs |
str |
The coordinate reference system code. Default is "EPSG:4326". |
'EPSG:4326' |
**kwargs |
Additional keyword arguments to be passed to the GeoDataFrame constructor. |
{} |
Returns:
Type | Description |
---|---|
geopandas.GeoDataFrame |
A GeoDataFrame containing the data from the H5 file(s). |
Exceptions:
Type | Description |
---|---|
ImportError |
Raised if h5py is not installed. |
ValueError |
Raised if the provided filenames argument is not a valid type or if a specified file does not exist. |
Examples:
>>> gdf = h5_to_gdf('data.h5', 'dataset1', 'lat', 'lon', columns=['column1', 'column2'], crs='EPSG:4326')
>>> print(gdf.head())
column1 column2 lat lon geometry
0 10 20 40.123456 -75.987654 POINT (-75.987654 40.123456)
1 15 25 40.234567 -75.876543 POINT (-75.876543 40.234567)
...
Source code in leafmap/common.py
def h5_to_gdf(
filenames: str,
dataset: str,
lat: str = "lat_lowestmode",
lon: str = "lon_lowestmode",
columns: Optional[List[str]] = None,
crs: str = "EPSG:4326",
nodata=None,
**kwargs,
):
"""
Read data from one or multiple HDF5 files and return as a GeoDataFrame.
Args:
filenames (str or List[str]): The filename(s) of the HDF5 file(s).
dataset (str): The dataset name within the H5 file(s).
lat (str): The column name representing latitude. Default is 'lat_lowestmode'.
lon (str): The column name representing longitude. Default is 'lon_lowestmode'.
columns (List[str], optional): List of column names to include. If None, all columns will be included. Default is None.
crs (str, optional): The coordinate reference system code. Default is "EPSG:4326".
**kwargs: Additional keyword arguments to be passed to the GeoDataFrame constructor.
Returns:
geopandas.GeoDataFrame: A GeoDataFrame containing the data from the H5 file(s).
Raises:
ImportError: Raised if h5py is not installed.
ValueError: Raised if the provided filenames argument is not a valid type or if a specified file does not exist.
Example:
>>> gdf = h5_to_gdf('data.h5', 'dataset1', 'lat', 'lon', columns=['column1', 'column2'], crs='EPSG:4326')
>>> print(gdf.head())
column1 column2 lat lon geometry
0 10 20 40.123456 -75.987654 POINT (-75.987654 40.123456)
1 15 25 40.234567 -75.876543 POINT (-75.876543 40.234567)
...
"""
try:
import h5py
except ImportError:
install_package("h5py")
import h5py
import glob
import pandas as pd
import geopandas as gpd
if isinstance(filenames, str):
if os.path.exists(filenames):
files = [filenames]
else:
files = glob.glob(filenames)
if not files:
raise ValueError(f"File {filenames} does not exist.")
files.sort()
elif isinstance(filenames, list):
files = filenames
else:
raise ValueError("h5_file must be a string or a list of strings.")
out_df = pd.DataFrame()
for file in files:
h5 = h5py.File(file, "r")
try:
data = h5[dataset]
except KeyError:
print(f"Dataset {dataset} not found in file {file}. Skipping...")
continue
col_names = []
col_val = []
for key, value in data.items():
if columns is None or key in columns or key == lat or key == lon:
col_names.append(key)
col_val.append(value[:].tolist())
df = pd.DataFrame(map(list, zip(*col_val)), columns=col_names)
out_df = pd.concat([out_df, df])
h5.close()
if nodata is not None and columns is not None:
out_df = out_df[out_df[columns[0]] != nodata]
gdf = gpd.GeoDataFrame(
out_df, geometry=gpd.points_from_xy(out_df[lon], out_df[lat]), crs=crs, **kwargs
)
return gdf
h5_variables(filename, key)
¶
Retrieve the variables (column names) within a specific key (dataset) in an H5 file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filename |
str |
The filename of the H5 file. |
required |
key |
str |
The key (dataset name) within the H5 file. |
required |
Returns:
Type | Description |
---|---|
List[str] |
A list of variable names (column names) within the specified key. |
Exceptions:
Type | Description |
---|---|
ImportError |
Raised if h5py is not installed. |
Examples:
>>> variables = h5_variables('data.h5', 'dataset1')
>>> print(variables)
['var1', 'var2', 'var3']
Source code in leafmap/common.py
def h5_variables(filename: str, key: str) -> List[str]:
"""
Retrieve the variables (column names) within a specific key (dataset) in an H5 file.
Args:
filename (str): The filename of the H5 file.
key (str): The key (dataset name) within the H5 file.
Returns:
List[str]: A list of variable names (column names) within the specified key.
Raises:
ImportError: Raised if h5py is not installed.
Example:
>>> variables = h5_variables('data.h5', 'dataset1')
>>> print(variables)
['var1', 'var2', 'var3']
"""
try:
import h5py
except ImportError:
raise ImportError(
"h5py must be installed to use this function. Please install it with 'pip install h5py'."
)
with h5py.File(filename, "r") as f:
cols = list(f[key].keys())
return cols
has_transparency(img)
¶
Checks whether an image has transparency.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
img |
object |
a PIL Image object. |
required |
Returns:
Type | Description |
---|---|
bool |
True if it has transparency, False otherwise. |
Source code in leafmap/common.py
def has_transparency(img) -> bool:
"""Checks whether an image has transparency.
Args:
img (object): a PIL Image object.
Returns:
bool: True if it has transparency, False otherwise.
"""
if img.mode == "P":
transparent = img.info.get("transparency", -1)
for _, index in img.getcolors():
if index == transparent:
return True
elif img.mode == "RGBA":
extrema = img.getextrema()
if extrema[3][0] < 255:
return True
return False
hex_to_rgb(value='FFFFFF')
¶
Converts hex color to RGB color.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
value |
str |
Hex color code as a string. Defaults to 'FFFFFF'. |
'FFFFFF' |
Returns:
Type | Description |
---|---|
tuple |
RGB color as a tuple. |
Source code in leafmap/common.py
def hex_to_rgb(value: Optional[str] = "FFFFFF") -> Tuple[int, int, int]:
"""Converts hex color to RGB color.
Args:
value (str, optional): Hex color code as a string. Defaults to 'FFFFFF'.
Returns:
tuple: RGB color as a tuple.
"""
value = value.lstrip("#")
lv = len(value)
return tuple(int(value[i : i + lv // 3], 16) for i in range(0, lv, lv // 3))
html_to_gradio(html, width='100%', height='500px', **kwargs)
¶
Converts the map to an HTML string that can be used in Gradio. Removes unsupported elements, such as attribution and any code blocks containing functions. See https://github.com/gradio-app/gradio/issues/3190
Parameters:
Name | Type | Description | Default |
---|---|---|---|
width |
str |
The width of the map. Defaults to '100%'. |
'100%' |
height |
str |
The height of the map. Defaults to '500px'. |
'500px' |
Returns:
Type | Description |
---|---|
str |
The HTML string to use in Gradio. |
Source code in leafmap/common.py
def html_to_gradio(html, width="100%", height="500px", **kwargs):
"""Converts the map to an HTML string that can be used in Gradio. Removes unsupported elements, such as
attribution and any code blocks containing functions. See https://github.com/gradio-app/gradio/issues/3190
Args:
width (str, optional): The width of the map. Defaults to '100%'.
height (str, optional): The height of the map. Defaults to '500px'.
Returns:
str: The HTML string to use in Gradio.
"""
if isinstance(width, int):
width = f"{width}px"
if isinstance(height, int):
height = f"{height}px"
if isinstance(html, str):
with open(html, "r") as f:
lines = f.readlines()
elif isinstance(html, list):
lines = html
else:
raise TypeError("html must be a file path or a list of strings")
output = []
skipped_lines = []
for index, line in enumerate(lines):
if index in skipped_lines:
continue
if line.lstrip().startswith('{"attribution":'):
continue
elif "on(L.Draw.Event.CREATED, function(e)" in line:
for i in range(14):
skipped_lines.append(index + i)
elif "L.Control.geocoder" in line:
for i in range(5):
skipped_lines.append(index + i)
elif "function(e)" in line:
print(
f"Warning: The folium plotting backend does not support functions in code blocks. Please delete line {index + 1}."
)
else:
output.append(line + "\n")
return f"""<iframe style="width: {width}; height: {height}" name="result" allow="midi; geolocation; microphone; camera;
display-capture; encrypted-media;" sandbox="allow-modals allow-forms
allow-scripts allow-same-origin allow-popups
allow-top-navigation-by-user-activation allow-downloads" allowfullscreen=""
allowpaymentrequest="" frameborder="0" srcdoc='{"".join(output)}'></iframe>"""
html_to_streamlit(html, width=800, height=600, responsive=True, scrolling=False, token_name=None, token_value=None, **kwargs)
¶
Renders an HTML file in a Streamlit app. This method is a static Streamlit Component, meaning, no information is passed back from Leaflet on browser interaction.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
html |
str |
The HTML file to render. It can a local file path or a URL. |
required |
width |
int |
Width of the map. Defaults to 800. |
800 |
height |
int |
Height of the map. Defaults to 600. |
600 |
responsive |
bool |
Whether to make the map responsive. Defaults to True. |
True |
scrolling |
bool |
Whether to allow the map to scroll. Defaults to False. |
False |
token_name |
str |
The name of the token in the HTML file to be replaced. Defaults to None. |
None |
token_value |
str |
The value of the token to pass to the HTML file. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
streamlit.components |
components.html object. |
Source code in leafmap/common.py
def html_to_streamlit(
html,
width=800,
height=600,
responsive=True,
scrolling=False,
token_name=None,
token_value=None,
**kwargs,
):
"""Renders an HTML file in a Streamlit app. This method is a static Streamlit Component, meaning, no information is passed back from Leaflet on browser interaction.
Args:
html (str): The HTML file to render. It can a local file path or a URL.
width (int, optional): Width of the map. Defaults to 800.
height (int, optional): Height of the map. Defaults to 600.
responsive (bool, optional): Whether to make the map responsive. Defaults to True.
scrolling (bool, optional): Whether to allow the map to scroll. Defaults to False.
token_name (str, optional): The name of the token in the HTML file to be replaced. Defaults to None.
token_value (str, optional): The value of the token to pass to the HTML file. Defaults to None.
Returns:
streamlit.components: components.html object.
"""
try:
import streamlit as st # pylint: disable=E0401
import streamlit.components.v1 as components # pylint: disable=E0401
if isinstance(html, str):
temp_path = None
if html.startswith("http") and html.endswith(".html"):
temp_path = temp_file_path(".html")
out_file = os.path.basename(temp_path)
out_dir = os.path.dirname(temp_path)
download_from_url(html, out_file, out_dir)
html = temp_path
elif not os.path.exists(html):
raise FileNotFoundError("The specified input html does not exist.")
with open(html) as f:
lines = f.readlines()
if (token_name is not None) and (token_value is not None):
lines = [line.replace(token_name, token_value) for line in lines]
html_str = "".join(lines)
if temp_path is not None:
os.remove(temp_path)
if responsive:
make_map_responsive = """
<style>
[title~="st.iframe"] { width: 100%}
</style>
"""
st.markdown(make_map_responsive, unsafe_allow_html=True)
return components.html(
html_str, width=width, height=height, scrolling=scrolling
)
else:
raise TypeError("The html must be a string.")
except Exception as e:
raise Exception(e)
image_bandcount(image, **kwargs)
¶
Get the number of bands in an image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
str |
The input image filepath or URL. |
required |
Returns:
Type | Description |
---|---|
int |
The number of bands in the image. |
Source code in leafmap/common.py
def image_bandcount(image, **kwargs):
"""Get the number of bands in an image.
Args:
image (str): The input image filepath or URL.
Returns:
int: The number of bands in the image.
"""
image_check(image)
if isinstance(image, str):
_, client = get_local_tile_layer(image, return_client=True, **kwargs)
else:
client = image
return len(client.metadata()["bands"])
image_bounds(image, **kwargs)
¶
Get the bounds of an image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
str |
The input image filepath or URL. |
required |
Returns:
Type | Description |
---|---|
list |
A list of bounds in the form of [(south, west), (north, east)]. |
Source code in leafmap/common.py
def image_bounds(image, **kwargs):
"""Get the bounds of an image.
Args:
image (str): The input image filepath or URL.
Returns:
list: A list of bounds in the form of [(south, west), (north, east)].
"""
image_check(image)
if isinstance(image, str):
_, client = get_local_tile_layer(image, return_client=True, **kwargs)
else:
client = image
bounds = client.bounds()
return [(bounds[0], bounds[2]), (bounds[1], bounds[3])]
image_center(image, **kwargs)
¶
Get the center of an image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
str |
The input image filepath or URL. |
required |
Returns:
Type | Description |
---|---|
tuple |
A tuple of (latitude, longitude). |
Source code in leafmap/common.py
def image_center(image, **kwargs):
"""Get the center of an image.
Args:
image (str): The input image filepath or URL.
Returns:
tuple: A tuple of (latitude, longitude).
"""
image_check(image)
if isinstance(image, str):
_, client = get_local_tile_layer(image, return_client=True, **kwargs)
else:
client = image
return client.center()
image_client(image, **kwargs)
¶
Get a LocalTileserver TileClient from an image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
str |
The input image filepath or URL. |
required |
Returns:
Type | Description |
---|---|
TileClient |
A LocalTileserver TileClient. |
Source code in leafmap/common.py
def image_client(image, **kwargs):
"""Get a LocalTileserver TileClient from an image.
Args:
image (str): The input image filepath or URL.
Returns:
TileClient: A LocalTileserver TileClient.
"""
image_check(image)
_, client = get_local_tile_layer(image, return_client=True, **kwargs)
return client
image_comparison(img1, img2, label1='1', label2='2', width=704, show_labels=True, starting_position=50, make_responsive=True, in_memory=True, out_html=None)
¶
Create a comparison slider for two images. The source code is adapted from https://github.com/fcakyon/streamlit-image-comparison. Credits to the GitHub user @fcakyon. Users can also use https://juxtapose.knightlab.com to create a comparison slider.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
img1 |
str |
Path to the first image. It can be a local file path, a URL, or a numpy array. |
required |
img2 |
str |
Path to the second image. It can be a local file path, a URL, or a numpy array. |
required |
label1 |
str |
Label for the first image. Defaults to "1". |
'1' |
label2 |
str |
Label for the second image. Defaults to "2". |
'2' |
width |
int |
Width of the component in pixels. Defaults to 704. |
704 |
show_labels |
bool |
Whether to show labels on the images. Default is True. |
True |
starting_position |
int |
Starting position of the slider as a percentage (0-100). Default is 50. |
50 |
make_responsive |
bool |
Whether to enable responsive mode. Default is True. |
True |
in_memory |
bool |
Whether to handle pillow to base64 conversion in memory without saving to local. Default is True. |
True |
out_html |
str |
Whether to handle pillow to base64 conversion in memory without saving to local. Default is True. |
None |
Source code in leafmap/common.py
def image_comparison(
img1: str,
img2: str,
label1: str = "1",
label2: str = "2",
width: int = 704,
show_labels: bool = True,
starting_position: int = 50,
make_responsive: bool = True,
in_memory: bool = True,
out_html: str = None,
):
"""Create a comparison slider for two images. The source code is adapted from
https://github.com/fcakyon/streamlit-image-comparison. Credits to the GitHub user @fcakyon.
Users can also use https://juxtapose.knightlab.com to create a comparison slider.
Args:
img1 (str): Path to the first image. It can be a local file path, a URL, or a numpy array.
img2 (str): Path to the second image. It can be a local file path, a URL, or a numpy array.
label1 (str, optional): Label for the first image. Defaults to "1".
label2 (str, optional): Label for the second image. Defaults to "2".
width (int, optional): Width of the component in pixels. Defaults to 704.
show_labels (bool, optional): Whether to show labels on the images. Default is True.
starting_position (int, optional): Starting position of the slider as a percentage (0-100). Default is 50.
make_responsive (bool, optional): Whether to enable responsive mode. Default is True.
in_memory (bool, optional): Whether to handle pillow to base64 conversion in memory without saving to local. Default is True.
out_html (str, optional): Whether to handle pillow to base64 conversion in memory without saving to local. Default is True.
"""
from PIL import Image
import base64
import io
import os
import uuid
from typing import Union
import requests
import tempfile
import numpy as np
from IPython.display import HTML, display
TEMP_DIR = os.path.join(tempfile.gettempdir(), random_string(6))
os.makedirs(TEMP_DIR, exist_ok=True)
def exif_transpose(image: Image.Image):
"""
Transpose a PIL image accordingly if it has an EXIF Orientation tag.
Inplace version of https://github.com/python-pillow/Pillow/blob/master/src/PIL/ImageOps.py exif_transpose()
:param image: The image to transpose.
:return: An image.
"""
exif = image.getexif()
orientation = exif.get(0x0112, 1) # default 1
if orientation > 1:
method = {
2: Image.FLIP_LEFT_RIGHT,
3: Image.ROTATE_180,
4: Image.FLIP_TOP_BOTTOM,
5: Image.TRANSPOSE,
6: Image.ROTATE_270,
7: Image.TRANSVERSE,
8: Image.ROTATE_90,
}.get(orientation)
if method is not None:
image = image.transpose(method)
del exif[0x0112]
image.info["exif"] = exif.tobytes()
return image
def read_image_as_pil(
image: Union[Image.Image, str, np.ndarray], exif_fix: bool = False
):
"""
Loads an image as PIL.Image.Image.
Args:
image : Can be image path or url (str), numpy image (np.ndarray) or PIL.Image
"""
# https://stackoverflow.com/questions/56174099/how-to-load-images-larger-than-max-image-pixels-with-pil
Image.MAX_IMAGE_PIXELS = None
if isinstance(image, Image.Image):
image_pil = image.convert("RGB")
elif isinstance(image, str):
# read image if str image path is provided
try:
image_pil = Image.open(
requests.get(image, stream=True).raw
if str(image).startswith("http")
else image
).convert("RGB")
if exif_fix:
image_pil = exif_transpose(image_pil)
except: # handle large/tiff image reading
try:
import skimage.io
except ImportError:
raise ImportError(
"Please run 'pip install -U scikit-image imagecodecs' for large image handling."
)
image_sk = skimage.io.imread(image).astype(np.uint8)
if len(image_sk.shape) == 2: # b&w
image_pil = Image.fromarray(image_sk, mode="1").convert("RGB")
elif image_sk.shape[2] == 4: # rgba
image_pil = Image.fromarray(image_sk, mode="RGBA").convert("RGB")
elif image_sk.shape[2] == 3: # rgb
image_pil = Image.fromarray(image_sk, mode="RGB")
else:
raise TypeError(
f"image with shape: {image_sk.shape[3]} is not supported."
)
elif isinstance(image, np.ndarray):
if image.shape[0] < 5: # image in CHW
image = image[:, :, ::-1]
image_pil = Image.fromarray(image).convert("RGB")
else:
raise TypeError("read image with 'pillow' using 'Image.open()'")
return image_pil
def pillow_to_base64(image: Image.Image) -> str:
"""
Convert a PIL image to a base64-encoded string.
Parameters
----------
image: PIL.Image.Image
The image to be converted.
Returns
-------
str
The base64-encoded string.
"""
in_mem_file = io.BytesIO()
image.save(in_mem_file, format="JPEG", subsampling=0, quality=100)
img_bytes = in_mem_file.getvalue() # bytes
image_str = base64.b64encode(img_bytes).decode("utf-8")
base64_src = f"data:image/jpg;base64,{image_str}"
return base64_src
def local_file_to_base64(image_path: str) -> str:
"""
Convert a local image file to a base64-encoded string.
Parameters
----------
image_path: str
The path to the image file.
Returns
-------
str
The base64-encoded string.
"""
file_ = open(image_path, "rb")
img_bytes = file_.read()
image_str = base64.b64encode(img_bytes).decode("utf-8")
file_.close()
base64_src = f"data:image/jpg;base64,{image_str}"
return base64_src
def pillow_local_file_to_base64(image: Image.Image, temp_dir: str):
"""
Convert a Pillow image to a base64 string, using a temporary file on disk.
Parameters
----------
image : PIL.Image.Image
The Pillow image to convert.
temp_dir : str
The directory to use for the temporary file.
Returns
-------
str
A base64-encoded string representing the image.
"""
# Create temporary file path using os.path.join()
img_path = os.path.join(temp_dir, str(uuid.uuid4()) + ".jpg")
# Save image to temporary file
image.save(img_path, subsampling=0, quality=100)
# Convert temporary file to base64 string
base64_src = local_file_to_base64(img_path)
return base64_src
# Prepare images
img1_pillow = read_image_as_pil(img1)
img2_pillow = read_image_as_pil(img2)
img_width, img_height = img1_pillow.size
h_to_w = img_height / img_width
height = int((width * h_to_w) * 0.95)
if in_memory:
# Convert images to base64 strings
img1 = pillow_to_base64(img1_pillow)
img2 = pillow_to_base64(img2_pillow)
else:
# Create base64 strings from temporary files
os.makedirs(TEMP_DIR, exist_ok=True)
for file_ in os.listdir(TEMP_DIR):
if file_.endswith(".jpg"):
os.remove(os.path.join(TEMP_DIR, file_))
img1 = pillow_local_file_to_base64(img1_pillow, TEMP_DIR)
img2 = pillow_local_file_to_base64(img2_pillow, TEMP_DIR)
# Load CSS and JS
cdn_path = "https://cdn.knightlab.com/libs/juxtapose/latest"
css_block = f'<link rel="stylesheet" href="{cdn_path}/css/juxtapose.css">'
js_block = f'<script src="{cdn_path}/js/juxtapose.min.js"></script>'
# write html block
htmlcode = f"""
<html>
<head>
<style>body {{ margin: unset; }}</style>
{css_block}
{js_block}
<div id="foo" style="height: {height}; width: {width or '100%'};"></div>
<script>
slider = new juxtapose.JXSlider('#foo',
[
{{
src: '{img1}',
label: '{label1}',
}},
{{
src: '{img2}',
label: '{label2}',
}}
],
{{
animate: true,
showLabels: {'true' if show_labels else 'false'},
showCredits: true,
startingPosition: "{starting_position}%",
makeResponsive: {'true' if make_responsive else 'false'},
}});
</script>
</head>
</html>
"""
if out_html is not None:
with open(out_html, "w") as f:
f.write(htmlcode)
shutil.rmtree(TEMP_DIR)
display(HTML(htmlcode))
image_filesize(region, cellsize, bands=1, dtype='uint8', unit='MB', source_crs='epsg:4326', dst_crs='epsg:3857', bbox=False)
¶
Calculate the size of an image in a given region and cell size.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
region |
list |
A bounding box in the format of [minx, miny, maxx, maxy]. |
required |
cellsize |
float |
The resolution of the image. |
required |
bands |
int |
Number of bands. Defaults to 1. |
1 |
dtype |
str |
Data type, such as unit8, float32. For more info, see https://numpy.org/doc/stable/user/basics.types.html. Defaults to 'uint8'. |
'uint8' |
unit |
str |
The unit of the output. Defaults to 'MB'. |
'MB' |
source_crs |
str |
The CRS of the region. Defaults to 'epsg:4326'. |
'epsg:4326' |
dst_crs |
str |
The destination CRS to calculate the area. Defaults to 'epsg:3857'. |
'epsg:3857' |
bbox |
bool |
Whether to use the bounding box of the region to calculate the area. Defaults to False. |
False |
Returns:
Type | Description |
---|---|
float |
The size of the image in a given unit. |
Source code in leafmap/common.py
def image_filesize(
region,
cellsize,
bands=1,
dtype="uint8",
unit="MB",
source_crs="epsg:4326",
dst_crs="epsg:3857",
bbox=False,
):
"""Calculate the size of an image in a given region and cell size.
Args:
region (list): A bounding box in the format of [minx, miny, maxx, maxy].
cellsize (float): The resolution of the image.
bands (int, optional): Number of bands. Defaults to 1.
dtype (str, optional): Data type, such as unit8, float32. For more info,
see https://numpy.org/doc/stable/user/basics.types.html. Defaults to 'uint8'.
unit (str, optional): The unit of the output. Defaults to 'MB'.
source_crs (str, optional): The CRS of the region. Defaults to 'epsg:4326'.
dst_crs (str, optional): The destination CRS to calculate the area. Defaults to 'epsg:3857'.
bbox (bool, optional): Whether to use the bounding box of the region to calculate the area. Defaults to False.
Returns:
float: The size of the image in a given unit.
"""
import numpy as np
import geopandas as gpd
if bbox:
if isinstance(region, gpd.GeoDataFrame):
region = region.to_crs(dst_crs).total_bounds.tolist()
elif isinstance(region, str) and os.path.exists(region):
region = gpd.read_file(region).to_crs(dst_crs).total_bounds.tolist()
elif isinstance(region, list):
region = (
bbox_to_gdf(region, crs=source_crs)
.to_crs(dst_crs)
.total_bounds.tolist()
)
else:
raise ValueError("Invalid input region.")
bytes = (
np.prod(
[
int((region[2] - region[0]) / cellsize),
int((region[3] - region[1]) / cellsize),
bands,
]
)
* np.dtype(dtype).itemsize
)
else:
if isinstance(region, list):
region = bbox_to_gdf(region, crs=source_crs)
bytes = (
vector_area(region, crs=dst_crs)
/ pow(cellsize, 2)
* np.dtype(dtype).itemsize
* bands
)
unit = unit.upper()
if unit == "KB":
return bytes / 1024
elif unit == "MB":
return bytes / pow(1024, 2)
elif unit == "GB":
return bytes / pow(1024, 3)
elif unit == "TB":
return bytes / pow(1024, 4)
elif unit == "PB":
return bytes / pow(1024, 5)
else:
return bytes
image_geotransform(image, **kwargs)
¶
Get the geotransform of an image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
str |
The input image filepath or URL. |
required |
Returns:
Type | Description |
---|---|
list |
A list of geotransform values. |
Source code in leafmap/common.py
def image_geotransform(image, **kwargs):
"""Get the geotransform of an image.
Args:
image (str): The input image filepath or URL.
Returns:
list: A list of geotransform values.
"""
image_check(image)
if isinstance(image, str):
_, client = get_local_tile_layer(image, return_client=True, **kwargs)
else:
client = image
return client.metadata()["GeoTransform"]
image_metadata(image, **kwargs)
¶
Get the metadata of an image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
str |
The input image filepath or URL. |
required |
Returns:
Type | Description |
---|---|
dict |
A dictionary of image metadata. |
Source code in leafmap/common.py
def image_metadata(image, **kwargs):
"""Get the metadata of an image.
Args:
image (str): The input image filepath or URL.
Returns:
dict: A dictionary of image metadata.
"""
image_check(image)
if isinstance(image, str):
_, client = get_local_tile_layer(image, return_client=True, **kwargs)
else:
client = image
return client.metadata
image_min_max(image, bands=None)
¶
Computes the minimum and maximum pixel values of an image.
This function opens an image file using xarray and rasterio, optionally selects specific bands, and then computes the minimum and maximum pixel values in the image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
str |
The path to the image file. |
required |
bands |
int or list |
The band or list of bands to select. If None, all bands are used. |
None |
Returns:
Type | Description |
---|---|
Tuple[float, float] |
The minimum and maximum pixel values in the image. |
Source code in leafmap/common.py
def image_min_max(
image: str, bands: Optional[Union[int, list]] = None
) -> Tuple[float, float]:
"""
Computes the minimum and maximum pixel values of an image.
This function opens an image file using xarray and rasterio, optionally
selects specific bands, and then computes the minimum and maximum pixel
values in the image.
Args:
image (str): The path to the image file.
bands (int or list, optional): The band or list of bands to select. If
None, all bands are used.
Returns:
Tuple[float, float]: The minimum and maximum pixel values in the image.
"""
import rioxarray
import xarray as xr
dataset = xr.open_dataset(image, engine="rasterio")
if bands is not None:
dataset = dataset.sel(band=bands)
vmin = dataset["band_data"].min().values.item()
vmax = dataset["band_data"].max().values.item()
return vmin, vmax
image_projection(image, **kwargs)
¶
Get the projection of an image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
str |
The input image filepath or URL. |
required |
Returns:
Type | Description |
---|---|
str |
The projection of the image. |
Source code in leafmap/common.py
def image_projection(image, **kwargs):
"""Get the projection of an image.
Args:
image (str): The input image filepath or URL.
Returns:
str: The projection of the image.
"""
image_check(image)
if isinstance(image, str):
_, client = get_local_tile_layer(image, return_client=True, **kwargs)
else:
client = image
return client.metadata()["Projection"]
image_resolution(image, **kwargs)
¶
Get the resolution of an image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
str |
The input image filepath or URL. |
required |
Returns:
Type | Description |
---|---|
float |
The resolution of the image. |
Source code in leafmap/common.py
def image_resolution(image, **kwargs):
"""Get the resolution of an image.
Args:
image (str): The input image filepath or URL.
Returns:
float: The resolution of the image.
"""
image_check(image)
if isinstance(image, str):
_, client = get_local_tile_layer(image, return_client=True, **kwargs)
else:
client = image
return client.metadata()["GeoTransform"][1]
image_set_crs(image, epsg)
¶
Define the CRS of an image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
str |
The input image filepath |
required |
epsg |
int |
The EPSG code of the CRS to set. |
required |
Source code in leafmap/common.py
def image_set_crs(image, epsg):
"""Define the CRS of an image.
Args:
image (str): The input image filepath
epsg (int): The EPSG code of the CRS to set.
"""
from rasterio.crs import CRS
import rasterio
with rasterio.open(image, "r+") as rds:
rds.crs = CRS.from_epsg(epsg)
image_size(image, **kwargs)
¶
Get the size (width, height) of an image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
str |
The input image filepath or URL. |
required |
Returns:
Type | Description |
---|---|
tuple |
A tuple of (width, height). |
Source code in leafmap/common.py
def image_size(image, **kwargs):
"""Get the size (width, height) of an image.
Args:
image (str): The input image filepath or URL.
Returns:
tuple: A tuple of (width, height).
"""
image_check(image)
if isinstance(image, str):
_, client = get_local_tile_layer(image, return_client=True, **kwargs)
else:
client = image
metadata = client.metadata()
return metadata["sourceSizeX"], metadata["sourceSizeY"]
image_to_cog(source, dst_path=None, profile='deflate', **kwargs)
¶
Converts an image to a COG file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source |
str |
A dataset path, URL or rasterio.io.DatasetReader object. |
required |
dst_path |
str |
An output dataset path or or PathLike object. Defaults to None. |
None |
profile |
str |
COG profile. More at https://cogeotiff.github.io/rio-cogeo/profile. Defaults to "deflate". |
'deflate' |
Exceptions:
Type | Description |
---|---|
ImportError |
If rio-cogeo is not installed. |
FileNotFoundError |
If the source file could not be found. |
Source code in leafmap/common.py
def image_to_cog(source, dst_path=None, profile="deflate", **kwargs):
"""Converts an image to a COG file.
Args:
source (str): A dataset path, URL or rasterio.io.DatasetReader object.
dst_path (str, optional): An output dataset path or or PathLike object. Defaults to None.
profile (str, optional): COG profile. More at https://cogeotiff.github.io/rio-cogeo/profile. Defaults to "deflate".
Raises:
ImportError: If rio-cogeo is not installed.
FileNotFoundError: If the source file could not be found.
"""
try:
from rio_cogeo.cogeo import cog_translate
from rio_cogeo.profiles import cog_profiles
except ImportError:
raise ImportError(
"The rio-cogeo package is not installed. Please install it with `pip install rio-cogeo` or `conda install rio-cogeo -c conda-forge`."
)
if not source.startswith("http"):
source = check_file_path(source)
if not os.path.exists(source):
raise FileNotFoundError("The provided input file could not be found.")
if dst_path is None:
if not source.startswith("http"):
dst_path = os.path.splitext(source)[0] + "_cog.tif"
else:
dst_path = temp_file_path(extension=".tif")
dst_path = check_file_path(dst_path)
dst_profile = cog_profiles.get(profile)
cog_translate(source, dst_path, dst_profile, **kwargs)
image_to_geotiff(image, dst_path, dtype=None, to_cog=True, **kwargs)
¶
Converts an image to a GeoTIFF file.
This function takes an image in the form of a rasterio.io.DatasetReader object, and writes it to a GeoTIFF file at the specified destination path. The data type of the output GeoTIFF can be specified. Additional keyword arguments can be passed to customize the GeoTIFF profile.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
DatasetReader |
The input image as a rasterio.io.DatasetReader object. |
required |
dst_path |
str |
The destination path where the GeoTIFF file will be saved. |
required |
dtype |
Optional[str] |
The data type for the output GeoTIFF file. If None, the data type of the input image will be used. Defaults to None. |
None |
to_cog |
bool |
Whether to convert the output GeoTIFF to a Cloud Optimized GeoTIFF (COG). Defaults to True. |
True |
**kwargs |
Additional keyword arguments to be included in the GeoTIFF profile. |
{} |
Exceptions:
Type | Description |
---|---|
ValueError |
If the input image is not a rasterio.io.DatasetReader object. |
Returns:
Type | Description |
---|---|
None |
None |
Source code in leafmap/common.py
def image_to_geotiff(image, dst_path, dtype=None, to_cog=True, **kwargs) -> None:
"""
Converts an image to a GeoTIFF file.
This function takes an image in the form of a rasterio.io.DatasetReader object, and writes it to a GeoTIFF file
at the specified destination path. The data type of the output GeoTIFF can be specified. Additional keyword
arguments can be passed to customize the GeoTIFF profile.
Args:
image (DatasetReader): The input image as a rasterio.io.DatasetReader object.
dst_path (str): The destination path where the GeoTIFF file will be saved.
dtype (Optional[str]): The data type for the output GeoTIFF file. If None, the data type of the input image
will be used. Defaults to None.
to_cog (bool): Whether to convert the output GeoTIFF to a Cloud Optimized GeoTIFF (COG). Defaults to True.
**kwargs: Additional keyword arguments to be included in the GeoTIFF profile.
Raises:
ValueError: If the input image is not a rasterio.io.DatasetReader object.
Returns:
None
"""
import rasterio
from rasterio.enums import Resampling
if not isinstance(image, rasterio.io.DatasetReader):
raise ValueError("The input image must be a rasterio.io.DatasetReader object.")
dst_path = check_file_path(dst_path)
profile = image.profile
if dtype is not None:
profile["dtype"] = dtype
for key, value in kwargs.items():
profile[key] = value
with rasterio.open(dst_path, "w", **profile) as dst:
dst.write(image.read())
if to_cog:
image_to_cog(dst_path, dst_path)
image_to_numpy(image)
¶
Converts an image to a numpy array.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
str |
A dataset path, URL or rasterio.io.DatasetReader object. |
required |
Exceptions:
Type | Description |
---|---|
FileNotFoundError |
If the provided file could not be found. |
Returns:
Type | Description |
---|---|
np.array |
A numpy array. |
Source code in leafmap/common.py
def image_to_numpy(image):
"""Converts an image to a numpy array.
Args:
image (str): A dataset path, URL or rasterio.io.DatasetReader object.
Raises:
FileNotFoundError: If the provided file could not be found.
Returns:
np.array: A numpy array.
"""
import rasterio
from osgeo import gdal
# ... and suppress errors
gdal.PushErrorHandler("CPLQuietErrorHandler")
try:
with rasterio.open(image, "r") as ds:
arr = ds.read() # read all raster values
return arr
except Exception as e:
raise Exception(e)
images_to_tiles(images, names=None, **kwargs)
¶
Convert a list of images to a dictionary of ipyleaflet.TileLayer objects.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
images |
str | list |
The path to a directory of images or a list of image paths. |
required |
names |
list |
A list of names for the layers. Defaults to None. |
None |
**kwargs |
Additional arguments to pass to get_local_tile_layer(). |
{} |
Returns:
Type | Description |
---|---|
dict |
A dictionary of ipyleaflet.TileLayer objects. |
Source code in leafmap/common.py
def images_to_tiles(
images: Union[str, List[str]], names: List[str] = None, **kwargs
) -> Dict[str, ipyleaflet.TileLayer]:
"""Convert a list of images to a dictionary of ipyleaflet.TileLayer objects.
Args:
images (str | list): The path to a directory of images or a list of image paths.
names (list, optional): A list of names for the layers. Defaults to None.
**kwargs: Additional arguments to pass to get_local_tile_layer().
Returns:
dict: A dictionary of ipyleaflet.TileLayer objects.
"""
tiles = {}
if isinstance(images, str):
images = os.path.abspath(images)
images = find_files(images, ext=".tif", recursive=False)
if not isinstance(images, list):
raise ValueError("images must be a list of image paths or a directory")
if names is None:
names = [os.path.splitext(os.path.basename(image))[0] for image in images]
if len(names) != len(images):
raise ValueError("names must have the same length as images")
for index, image in enumerate(images):
name = names[index]
try:
if image.startswith("http") and image.endswith(".tif"):
url = cog_tile(image, **kwargs)
tile = ipyleaflet.TileLayer(url=url, name=name, **kwargs)
elif image.startswith("http"):
url = stac_tile(image, **kwargs)
tile = ipyleaflet.TileLayer(url=url, name=name, **kwargs)
else:
tile = get_local_tile_layer(image, layer_name=name, **kwargs)
tiles[name] = tile
except Exception as e:
print(image, e)
return tiles
install_package(package)
¶
Install a Python package.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
package |
str | list |
The package name or a GitHub URL or a list of package names or GitHub URLs. |
required |
Source code in leafmap/common.py
def install_package(package):
"""Install a Python package.
Args:
package (str | list): The package name or a GitHub URL or a list of package names or GitHub URLs.
"""
import subprocess
if isinstance(package, str):
packages = [package]
for package in packages:
if package.startswith("https"):
package = f"git+{package}"
# Execute pip install command and show output in real-time
command = f"pip install {package}"
process = subprocess.Popen(command.split(), stdout=subprocess.PIPE)
# Print output in real-time
while True:
output = process.stdout.readline()
if output == b"" and process.poll() is not None:
break
if output:
print(output.decode("utf-8").strip())
# Wait for process to complete
process.wait()
is_arcpy()
¶
Check if arcpy is available.
Returns:
Type | Description |
---|---|
book |
True if arcpy is available, False otherwise. |
Source code in leafmap/common.py
def is_arcpy():
"""Check if arcpy is available.
Returns:
book: True if arcpy is available, False otherwise.
"""
import sys
if "arcpy" in sys.modules:
return True
else:
return False
is_array(x)
¶
Test whether x is either a numpy.ndarray or xarray.DataArray
Source code in leafmap/common.py
def is_array(x):
"""Test whether x is either a numpy.ndarray or xarray.DataArray"""
import sys
if isinstance(x, sys.modules["numpy"].ndarray):
return True
if "xarray" in sys.modules:
if isinstance(x, sys.modules["xarray"].DataArray):
return True
return False
is_jupyterlite()
¶
Check if the current notebook is running on JupyterLite.
Returns:
Type | Description |
---|---|
book |
True if the notebook is running on JupyterLite. |
Source code in leafmap/common.py
def is_jupyterlite():
"""Check if the current notebook is running on JupyterLite.
Returns:
book: True if the notebook is running on JupyterLite.
"""
import sys
if "pyodide" in sys.modules:
return True
else:
return False
is_on_aws()
¶
Check if the current notebook is running on AWS.
Returns:
Type | Description |
---|---|
bool |
True if the notebook is running on AWS. |
Source code in leafmap/common.py
def is_on_aws():
"""Check if the current notebook is running on AWS.
Returns:
bool: True if the notebook is running on AWS.
"""
import psutil
on_aws = False
try:
output = psutil.Process().parent().cmdline()
for item in output:
if item.endswith(".aws") or "ec2-user" in item:
on_aws = True
except:
pass
return on_aws
is_studio_lab()
¶
Check if the current notebook is running on Studio Lab.
Returns:
Type | Description |
---|---|
bool |
True if the notebook is running on Studio Lab. |
Source code in leafmap/common.py
def is_studio_lab():
"""Check if the current notebook is running on Studio Lab.
Returns:
bool: True if the notebook is running on Studio Lab.
"""
import psutil
on_studio_lab = False
try:
output = psutil.Process().parent().cmdline()
for item in output:
if "studiolab/bin" in item:
on_studio_lab = True
except:
pass
return on_studio_lab
is_tool(name)
¶
Check whether name
is on PATH and marked as executable.
Source code in leafmap/common.py
def is_tool(name):
"""Check whether `name` is on PATH and marked as executable."""
return shutil.which(name) is not None
kml_to_geojson(in_kml, out_geojson=None)
¶
Converts a KML to GeoJSON.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_kml |
str |
The file path to the input KML. |
required |
out_geojson |
str |
The file path to the output GeoJSON. Defaults to None. |
None |
Exceptions:
Type | Description |
---|---|
FileNotFoundError |
The input KML could not be found. |
TypeError |
The output must be a GeoJSON. |
Source code in leafmap/common.py
def kml_to_geojson(in_kml, out_geojson=None):
"""Converts a KML to GeoJSON.
Args:
in_kml (str): The file path to the input KML.
out_geojson (str): The file path to the output GeoJSON. Defaults to None.
Raises:
FileNotFoundError: The input KML could not be found.
TypeError: The output must be a GeoJSON.
"""
warnings.filterwarnings("ignore")
in_kml = os.path.abspath(in_kml)
if not os.path.exists(in_kml):
raise FileNotFoundError("The input KML could not be found.")
if out_geojson is not None:
out_geojson = os.path.abspath(out_geojson)
ext = os.path.splitext(out_geojson)[1].lower()
if ext not in [".json", ".geojson"]:
raise TypeError("The output file must be a GeoJSON.")
out_dir = os.path.dirname(out_geojson)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
check_package(name="geopandas", URL="https://geopandas.org")
import geopandas as gpd
import fiona
# import fiona
# print(fiona.supported_drivers)
fiona.drvsupport.supported_drivers["KML"] = "rw"
gdf = gpd.read_file(in_kml, driver="KML")
if out_geojson is not None:
gdf.to_file(out_geojson, driver="GeoJSON")
else:
return gdf.__geo_interface__
kml_to_shp(in_kml, out_shp)
¶
Converts a KML to shapefile.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_kml |
str |
The file path to the input KML. |
required |
out_shp |
str |
The file path to the output shapefile. |
required |
Exceptions:
Type | Description |
---|---|
FileNotFoundError |
The input KML could not be found. |
TypeError |
The output must be a shapefile. |
Source code in leafmap/common.py
def kml_to_shp(in_kml, out_shp):
"""Converts a KML to shapefile.
Args:
in_kml (str): The file path to the input KML.
out_shp (str): The file path to the output shapefile.
Raises:
FileNotFoundError: The input KML could not be found.
TypeError: The output must be a shapefile.
"""
warnings.filterwarnings("ignore")
in_kml = os.path.abspath(in_kml)
if not os.path.exists(in_kml):
raise FileNotFoundError("The input KML could not be found.")
out_shp = os.path.abspath(out_shp)
if not out_shp.endswith(".shp"):
raise TypeError("The output must be a shapefile.")
out_dir = os.path.dirname(out_shp)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
check_package(name="geopandas", URL="https://geopandas.org")
import geopandas as gpd
import fiona
# print(fiona.supported_drivers)
fiona.drvsupport.supported_drivers["KML"] = "rw"
df = gpd.read_file(in_kml, driver="KML")
df.to_file(out_shp)
list_palettes(add_extra=False, lowercase=False)
¶
List all available colormaps. See a complete lost of colormaps at https://matplotlib.org/stable/tutorials/colors/colormaps.html.
Returns:
Type | Description |
---|---|
list |
The list of colormap names. |
Source code in leafmap/common.py
def list_palettes(add_extra=False, lowercase=False):
"""List all available colormaps. See a complete lost of colormaps at https://matplotlib.org/stable/tutorials/colors/colormaps.html.
Returns:
list: The list of colormap names.
"""
import matplotlib.pyplot as plt
result = plt.colormaps()
if add_extra:
result += ["dem", "ndvi", "ndwi"]
if lowercase:
result = [i.lower() for i in result]
result.sort()
return result
lnglat_to_meters(longitude, latitude)
¶
coordinate conversion between lat/lon in decimal degrees to web mercator
Parameters:
Name | Type | Description | Default |
---|---|---|---|
longitude |
float |
The longitude. |
required |
latitude |
float |
The latitude. |
required |
Returns:
Type | Description |
---|---|
tuple |
A tuple of (x, y) in meters. |
Source code in leafmap/common.py
def lnglat_to_meters(longitude, latitude):
"""coordinate conversion between lat/lon in decimal degrees to web mercator
Args:
longitude (float): The longitude.
latitude (float): The latitude.
Returns:
tuple: A tuple of (x, y) in meters.
"""
import numpy as np
origin_shift = np.pi * 6378137
easting = longitude * origin_shift / 180.0
northing = np.log(np.tan((90 + latitude) * np.pi / 360.0)) * origin_shift / np.pi
if np.isnan(easting):
if longitude > 0:
easting = 20026376
else:
easting = -20026376
if np.isnan(northing):
if latitude > 0:
northing = 20048966
else:
northing = -20048966
return (easting, northing)
local_tile_bands(source)
¶
Get band names from COG.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source |
str | TileClient |
A local COG file path or TileClient |
required |
Returns:
Type | Description |
---|---|
list |
A list of band names. |
Source code in leafmap/common.py
def local_tile_bands(source):
"""Get band names from COG.
Args:
source (str | TileClient): A local COG file path or TileClient
Returns:
list: A list of band names.
"""
check_package("localtileserver", "https://github.com/banesullivan/localtileserver")
from localtileserver import TileClient
if isinstance(source, str):
tile_client = TileClient(source)
elif isinstance(source, TileClient):
tile_client = source
else:
raise ValueError("source must be a string or TileClient object.")
return tile_client.band_names
local_tile_pixel_value(lon, lat, tile_client, verbose=True, **kwargs)
¶
Get pixel value from COG.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
lon |
float |
Longitude of the pixel. |
required |
lat |
float |
Latitude of the pixel. |
required |
url |
str |
HTTP URL to a COG, e.g., 'https://github.com/opengeos/data/releases/download/raster/Libya-2023-07-01.tif' |
required |
bidx |
str |
Dataset band indexes (e.g bidx=1, bidx=1&bidx=2&bidx=3). Defaults to None. |
required |
titiler_endpoint |
str |
Titiler endpoint, e.g., "https://titiler.xyz", "planetary-computer", "pc". Defaults to None. |
required |
verbose |
bool |
Print status messages. Defaults to True. |
True |
Returns:
Type | Description |
---|---|
PointData |
rio-tiler point data. |
Source code in leafmap/common.py
def local_tile_pixel_value(
lon,
lat,
tile_client,
verbose=True,
**kwargs,
):
"""Get pixel value from COG.
Args:
lon (float): Longitude of the pixel.
lat (float): Latitude of the pixel.
url (str): HTTP URL to a COG, e.g., 'https://github.com/opengeos/data/releases/download/raster/Libya-2023-07-01.tif'
bidx (str, optional): Dataset band indexes (e.g bidx=1, bidx=1&bidx=2&bidx=3). Defaults to None.
titiler_endpoint (str, optional): Titiler endpoint, e.g., "https://titiler.xyz", "planetary-computer", "pc". Defaults to None.
verbose (bool, optional): Print status messages. Defaults to True.
Returns:
PointData: rio-tiler point data.
"""
return tile_client.point(lon, lat, coord_crs="EPSG:4326", **kwargs)
local_tile_vmin_vmax(source, bands=None, **kwargs)
¶
Get vmin and vmax from COG.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source |
str | TileClient |
A local COG file path or TileClient object. |
required |
bands |
str | list |
A list of band names. Defaults to None. |
None |
Exceptions:
Type | Description |
---|---|
ValueError |
If source is not a TileClient object or a local COG file path. |
Returns:
Type | Description |
---|---|
tuple |
A tuple of vmin and vmax. |
Source code in leafmap/common.py
def local_tile_vmin_vmax(
source,
bands=None,
**kwargs,
):
"""Get vmin and vmax from COG.
Args:
source (str | TileClient): A local COG file path or TileClient object.
bands (str | list, optional): A list of band names. Defaults to None.
Raises:
ValueError: If source is not a TileClient object or a local COG file path.
Returns:
tuple: A tuple of vmin and vmax.
"""
check_package("localtileserver", "https://github.com/banesullivan/localtileserver")
from localtileserver import TileClient
if isinstance(source, str):
tile_client = TileClient(source)
elif isinstance(source, TileClient):
tile_client = source
else:
raise ValueError("source must be a string or TileClient object.")
bandnames = tile_client.band_names
stats = tile_client.reader.statistics()
if isinstance(bands, str):
bands = [bands]
elif isinstance(bands, list):
pass
elif bands is None:
bands = bandnames
if all(b in bandnames for b in bands):
vmin = min([stats[b]["min"] for b in bands])
vmax = max([stats[b]["max"] for b in bands])
else:
vmin = min([stats[b]["min"] for b in bandnames])
vmax = max([stats[b]["max"] for b in bandnames])
return vmin, vmax
make_gif(images, out_gif, ext='jpg', fps=10, loop=0, mp4=False, clean_up=False)
¶
Creates a gif from a list of images.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
images |
list | str |
The list of images or input directory to create the gif from. |
required |
out_gif |
str |
File path to the output gif. |
required |
ext |
str |
The extension of the images. Defaults to 'jpg'. |
'jpg' |
fps |
int |
The frames per second of the gif. Defaults to 10. |
10 |
loop |
int |
The number of times to loop the gif. Defaults to 0. |
0 |
mp4 |
bool |
Whether to convert the gif to mp4. Defaults to False. |
False |
Source code in leafmap/common.py
def make_gif(images, out_gif, ext="jpg", fps=10, loop=0, mp4=False, clean_up=False):
"""Creates a gif from a list of images.
Args:
images (list | str): The list of images or input directory to create the gif from.
out_gif (str): File path to the output gif.
ext (str, optional): The extension of the images. Defaults to 'jpg'.
fps (int, optional): The frames per second of the gif. Defaults to 10.
loop (int, optional): The number of times to loop the gif. Defaults to 0.
mp4 (bool, optional): Whether to convert the gif to mp4. Defaults to False.
"""
import glob
from PIL import Image
ext = ext.replace(".", "")
if isinstance(images, str) and os.path.isdir(images):
images = list(glob.glob(os.path.join(images, f"*.{ext}")))
if len(images) == 0:
raise ValueError("No images found in the input directory.")
elif not isinstance(images, list):
raise ValueError("images must be a list or a path to the image directory.")
images.sort()
frames = [Image.open(image) for image in images]
frame_one = frames[0]
frame_one.save(
out_gif,
format="GIF",
append_images=frames,
save_all=True,
duration=int(1000 / fps),
loop=loop,
)
if mp4:
if not is_tool("ffmpeg"):
print("ffmpeg is not installed on your computer.")
return
if os.path.exists(out_gif):
out_mp4 = out_gif.replace(".gif", ".mp4")
cmd = f"ffmpeg -loglevel error -i {out_gif} -vcodec libx264 -crf 25 -pix_fmt yuv420p {out_mp4}"
os.system(cmd)
if not os.path.exists(out_mp4):
raise Exception(f"Failed to create mp4 file.")
if clean_up:
for image in images:
os.remove(image)
map_tiles_to_geotiff(output, bbox, zoom=None, resolution=None, source='OpenStreetMap', crs='EPSG:3857', to_cog=False, quiet=False, **kwargs)
¶
Download map tiles and convert them to a GeoTIFF. The source is adapted from https://github.com/gumblex/tms2geotiff. Credits to the GitHub user @gumblex.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
output |
str |
The output GeoTIFF file. |
required |
bbox |
list |
The bounding box [minx, miny, maxx, maxy] coordinates in EPSG:4326, e.g., [-122.5216, 37.733, -122.3661, 37.8095] |
required |
zoom |
int |
The map zoom level. Defaults to None. |
None |
resolution |
float |
The resolution in meters. Defaults to None. |
None |
source |
str |
The tile source. It can be one of the following: "OPENSTREETMAP", "ROADMAP", "SATELLITE", "TERRAIN", "HYBRID", or an HTTP URL. Defaults to "OpenStreetMap". |
'OpenStreetMap' |
crs |
str |
The coordinate reference system. Defaults to "EPSG:3857". |
'EPSG:3857' |
to_cog |
bool |
Convert to Cloud Optimized GeoTIFF. Defaults to False. |
False |
quiet |
bool |
Suppress output. Defaults to False. |
False |
**kwargs |
Additional arguments to pass to gdal.GetDriverByName("GTiff").Create(). |
{} |
Source code in leafmap/common.py
def map_tiles_to_geotiff(
output,
bbox,
zoom=None,
resolution=None,
source="OpenStreetMap",
crs="EPSG:3857",
to_cog=False,
quiet=False,
**kwargs,
):
"""Download map tiles and convert them to a GeoTIFF. The source is adapted from https://github.com/gumblex/tms2geotiff.
Credits to the GitHub user @gumblex.
Args:
output (str): The output GeoTIFF file.
bbox (list): The bounding box [minx, miny, maxx, maxy] coordinates in EPSG:4326, e.g., [-122.5216, 37.733, -122.3661, 37.8095]
zoom (int, optional): The map zoom level. Defaults to None.
resolution (float, optional): The resolution in meters. Defaults to None.
source (str, optional): The tile source. It can be one of the following: "OPENSTREETMAP", "ROADMAP",
"SATELLITE", "TERRAIN", "HYBRID", or an HTTP URL. Defaults to "OpenStreetMap".
crs (str, optional): The coordinate reference system. Defaults to "EPSG:3857".
to_cog (bool, optional): Convert to Cloud Optimized GeoTIFF. Defaults to False.
quiet (bool, optional): Suppress output. Defaults to False.
**kwargs: Additional arguments to pass to gdal.GetDriverByName("GTiff").Create().
"""
import re
import io
import math
import itertools
import concurrent.futures
import numpy
from PIL import Image
try:
from osgeo import gdal, osr
except ImportError:
raise ImportError("GDAL is not installed. Install it with pip install GDAL")
try:
import httpx
SESSION = httpx.Client()
except ImportError:
import requests
SESSION = requests.Session()
SESSION.headers.update(
{
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0",
}
)
xyz_tiles = {
"OPENSTREETMAP": {
"url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
"attribution": "OpenStreetMap",
"name": "OpenStreetMap",
},
"ROADMAP": {
"url": "https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}",
"attribution": "Google",
"name": "Google Maps",
},
"SATELLITE": {
"url": "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}",
"attribution": "Google",
"name": "Google Satellite",
},
"TERRAIN": {
"url": "https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}",
"attribution": "Google",
"name": "Google Terrain",
},
"HYBRID": {
"url": "https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}",
"attribution": "Google",
"name": "Google Satellite",
},
}
if isinstance(source, str) and source.upper() in xyz_tiles:
source = xyz_tiles[source.upper()]["url"]
elif isinstance(source, str) and source.startswith("http"):
pass
elif isinstance(source, str):
tiles = basemap_xyz_tiles()
if source in tiles:
source = tiles[source].url
else:
raise ValueError(
'source must be one of "OpenStreetMap", "ROADMAP", "SATELLITE", "TERRAIN", "HYBRID", or a URL'
)
def resolution_to_zoom_level(resolution):
"""
Convert map resolution in meters to zoom level for Web Mercator (EPSG:3857) tiles.
"""
# Web Mercator tile size in meters at zoom level 0
initial_resolution = 156543.03392804097
# Calculate the zoom level
zoom_level = math.log2(initial_resolution / resolution)
return int(zoom_level)
if isinstance(bbox, list) and len(bbox) == 4:
west, south, east, north = bbox
else:
raise ValueError(
"bbox must be a list of 4 coordinates in the format of [xmin, ymin, xmax, ymax]"
)
if zoom is None and resolution is None:
raise ValueError("Either zoom or resolution must be provided")
elif zoom is not None and resolution is not None:
raise ValueError("Only one of zoom or resolution can be provided")
if resolution is not None:
zoom = resolution_to_zoom_level(resolution)
EARTH_EQUATORIAL_RADIUS = 6378137.0
Image.MAX_IMAGE_PIXELS = None
gdal.UseExceptions()
web_mercator = osr.SpatialReference()
try:
web_mercator.ImportFromEPSG(3857)
except RuntimeError as e:
# https://github.com/PDAL/PDAL/issues/2544#issuecomment-637995923
if "PROJ" in str(e):
pattern = r"/[\w/]+"
match = re.search(pattern, str(e))
if match:
file_path = match.group(0)
os.environ["PROJ_LIB"] = file_path
os.environ["GDAL_DATA"] = file_path.replace("proj", "gdal")
web_mercator.ImportFromEPSG(3857)
WKT_3857 = web_mercator.ExportToWkt()
def from4326_to3857(lat, lon):
xtile = math.radians(lon) * EARTH_EQUATORIAL_RADIUS
ytile = (
math.log(math.tan(math.radians(45 + lat / 2.0))) * EARTH_EQUATORIAL_RADIUS
)
return (xtile, ytile)
def deg2num(lat, lon, zoom):
lat_r = math.radians(lat)
n = 2**zoom
xtile = (lon + 180) / 360 * n
ytile = (1 - math.log(math.tan(lat_r) + 1 / math.cos(lat_r)) / math.pi) / 2 * n
return (xtile, ytile)
def is_empty(im):
extrema = im.getextrema()
if len(extrema) >= 3:
if len(extrema) > 3 and extrema[-1] == (0, 0):
return True
for ext in extrema[:3]:
if ext != (0, 0):
return False
return True
else:
return extrema[0] == (0, 0)
def paste_tile(bigim, base_size, tile, corner_xy, bbox):
if tile is None:
return bigim
im = Image.open(io.BytesIO(tile))
mode = "RGB" if im.mode == "RGB" else "RGBA"
size = im.size
if bigim is None:
base_size[0] = size[0]
base_size[1] = size[1]
newim = Image.new(
mode, (size[0] * (bbox[2] - bbox[0]), size[1] * (bbox[3] - bbox[1]))
)
else:
newim = bigim
dx = abs(corner_xy[0] - bbox[0])
dy = abs(corner_xy[1] - bbox[1])
xy0 = (size[0] * dx, size[1] * dy)
if mode == "RGB":
newim.paste(im, xy0)
else:
if im.mode != mode:
im = im.convert(mode)
if not is_empty(im):
newim.paste(im, xy0)
im.close()
return newim
def finish_picture(bigim, base_size, bbox, x0, y0, x1, y1):
xfrac = x0 - bbox[0]
yfrac = y0 - bbox[1]
x2 = round(base_size[0] * xfrac)
y2 = round(base_size[1] * yfrac)
imgw = round(base_size[0] * (x1 - x0))
imgh = round(base_size[1] * (y1 - y0))
retim = bigim.crop((x2, y2, x2 + imgw, y2 + imgh))
if retim.mode == "RGBA" and retim.getextrema()[3] == (255, 255):
retim = retim.convert("RGB")
bigim.close()
return retim
def get_tile(url):
retry = 3
while 1:
try:
r = SESSION.get(url, timeout=60)
break
except Exception:
retry -= 1
if not retry:
raise
if r.status_code == 404:
return None
elif not r.content:
return None
r.raise_for_status()
return r.content
def draw_tile(
source, lat0, lon0, lat1, lon1, zoom, filename, quiet=False, **kwargs
):
x0, y0 = deg2num(lat0, lon0, zoom)
x1, y1 = deg2num(lat1, lon1, zoom)
x0, x1 = sorted([x0, x1])
y0, y1 = sorted([y0, y1])
corners = tuple(
itertools.product(
range(math.floor(x0), math.ceil(x1)),
range(math.floor(y0), math.ceil(y1)),
)
)
totalnum = len(corners)
futures = []
with concurrent.futures.ThreadPoolExecutor(5) as executor:
for x, y in corners:
futures.append(
executor.submit(get_tile, source.format(z=zoom, x=x, y=y))
)
bbox = (math.floor(x0), math.floor(y0), math.ceil(x1), math.ceil(y1))
bigim = None
base_size = [256, 256]
for k, (fut, corner_xy) in enumerate(zip(futures, corners), 1):
bigim = paste_tile(bigim, base_size, fut.result(), corner_xy, bbox)
if not quiet:
print("Downloaded image %d/%d" % (k, totalnum))
if not quiet:
print("Saving GeoTIFF. Please wait...")
img = finish_picture(bigim, base_size, bbox, x0, y0, x1, y1)
imgbands = len(img.getbands())
driver = gdal.GetDriverByName("GTiff")
if "options" not in kwargs:
kwargs["options"] = [
"COMPRESS=DEFLATE",
"PREDICTOR=2",
"ZLEVEL=9",
"TILED=YES",
]
kwargs.pop("overwrite", None)
gtiff = driver.Create(
filename,
img.size[0],
img.size[1],
imgbands,
gdal.GDT_Byte,
**kwargs,
)
xp0, yp0 = from4326_to3857(lat0, lon0)
xp1, yp1 = from4326_to3857(lat1, lon1)
pwidth = abs(xp1 - xp0) / img.size[0]
pheight = abs(yp1 - yp0) / img.size[1]
gtiff.SetGeoTransform((min(xp0, xp1), pwidth, 0, max(yp0, yp1), 0, -pheight))
gtiff.SetProjection(WKT_3857)
for band in range(imgbands):
array = np.array(img.getdata(band), dtype="u8")
array = array.reshape((img.size[1], img.size[0]))
band = gtiff.GetRasterBand(band + 1)
band.WriteArray(array)
gtiff.FlushCache()
if not quiet:
print(f"Image saved to {filename}")
return img
try:
draw_tile(source, south, west, north, east, zoom, output, quiet, **kwargs)
if crs.upper() != "EPSG:3857":
reproject(output, output, crs, to_cog=to_cog)
elif to_cog:
image_to_cog(output, output)
except Exception as e:
raise Exception(e)
mbtiles_to_pmtiles(input_file, output_file, max_zoom=99)
¶
Converts mbtiles to pmtiles using the pmtiles package.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
input_file |
str |
Path to the input .mbtiles file. |
required |
output_file |
str |
Path to the output .pmtiles file. |
required |
max_zoom |
int |
Maximum zoom level for the conversion. Defaults to 99. |
99 |
Returns:
Type | Description |
---|---|
None |
The function returns None either upon successful completion or when the pmtiles package is not installed. |
Source code in leafmap/common.py
def mbtiles_to_pmtiles(
input_file: str, output_file: str, max_zoom: int = 99
) -> Optional[None]:
"""
Converts mbtiles to pmtiles using the pmtiles package.
Args:
input_file (str): Path to the input .mbtiles file.
output_file (str): Path to the output .pmtiles file.
max_zoom (int): Maximum zoom level for the conversion. Defaults to 99.
Returns:
None: The function returns None either upon successful completion or when the pmtiles package is not installed.
Raises:
Any exception raised by pmtiles.convert.mbtiles_to_pmtiles will be propagated up.
"""
import pmtiles.convert as convert
convert.mbtiles_to_pmtiles(input_file, output_file, maxzoom=max_zoom)
merge_gifs(in_gifs, out_gif)
¶
Merge multiple gifs into one.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_gifs |
str | list |
The input gifs as a list or a directory path. |
required |
out_gif |
str |
The output gif. |
required |
Exceptions:
Type | Description |
---|---|
Exception |
Raise exception when gifsicle is not installed. |
Source code in leafmap/common.py
def merge_gifs(in_gifs, out_gif):
"""Merge multiple gifs into one.
Args:
in_gifs (str | list): The input gifs as a list or a directory path.
out_gif (str): The output gif.
Raises:
Exception: Raise exception when gifsicle is not installed.
"""
import glob
try:
if isinstance(in_gifs, str) and os.path.isdir(in_gifs):
in_gifs = glob.glob(os.path.join(in_gifs, "*.gif"))
elif not isinstance(in_gifs, list):
raise Exception("in_gifs must be a list.")
in_gifs = " ".join(in_gifs)
cmd = f"gifsicle {in_gifs} > {out_gif}"
os.system(cmd)
except Exception as e:
print(
"gifsicle is not installed. Run 'sudo apt-get install -y gifsicle' to install it."
)
print(e)
merge_rasters(input_dir, output, input_pattern='*.tif', output_format='GTiff', output_nodata=None, output_options=['COMPRESS=DEFLATE'])
¶
Merge a directory of rasters into a single raster.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
input_dir |
str |
The path to the input directory. |
required |
output |
str |
The path to the output raster. |
required |
input_pattern |
str |
The pattern to match the input files. Defaults to "*.tif". |
'*.tif' |
output_format |
str |
The output format. Defaults to "GTiff". |
'GTiff' |
output_nodata |
float |
The output nodata value. Defaults to None. |
None |
output_options |
list |
A list of output options. Defaults to ["COMPRESS=DEFLATE"]. |
['COMPRESS=DEFLATE'] |
Exceptions:
Type | Description |
---|---|
ImportError |
Raised if GDAL is not installed. |
Source code in leafmap/common.py
def merge_rasters(
input_dir,
output,
input_pattern="*.tif",
output_format="GTiff",
output_nodata=None,
output_options=["COMPRESS=DEFLATE"],
):
"""Merge a directory of rasters into a single raster.
Args:
input_dir (str): The path to the input directory.
output (str): The path to the output raster.
input_pattern (str, optional): The pattern to match the input files. Defaults to "*.tif".
output_format (str, optional): The output format. Defaults to "GTiff".
output_nodata (float, optional): The output nodata value. Defaults to None.
output_options (list, optional): A list of output options. Defaults to ["COMPRESS=DEFLATE"].
Raises:
ImportError: Raised if GDAL is not installed.
"""
import glob
try:
from osgeo import gdal
except ImportError:
raise ImportError(
"GDAL is required to use this function. Install it with `conda install gdal -c conda-forge`"
)
# Get a list of all the input files
input_files = glob.glob(os.path.join(input_dir, input_pattern))
# Merge the input files into a single output file
gdal.Warp(
output,
input_files,
format=output_format,
dstNodata=output_nodata,
options=output_options,
)
merge_vector(files, output, crs=None, ext='geojson', recursive=False, quiet=False, return_gdf=False, **kwargs)
¶
Merge vector files into a single GeoDataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
files |
Union[str, List[str]] |
A string or a list of file paths to be merged. |
required |
output |
str |
The file path to save the merged GeoDataFrame. |
required |
crs |
str |
Optional. The coordinate reference system (CRS) of the output GeoDataFrame. |
None |
ext |
str |
Optional. The file extension of the input files. Default is 'geojson'. |
'geojson' |
recursive |
bool |
Optional. If True, search for files recursively in subdirectories. Default is False. |
False |
quiet |
bool |
Optional. If True, suppresses progress messages. Default is False. |
False |
return_gdf |
bool |
Optional. If True, returns the merged GeoDataFrame. Default is False. |
False |
**kwargs |
Additional keyword arguments to be passed to the |
{} |
Returns:
Type | Description |
---|---|
If |
Exceptions:
Type | Description |
---|---|
TypeError |
If |
Source code in leafmap/common.py
def merge_vector(
files: Union[str, List[str]],
output: str,
crs: str = None,
ext: str = "geojson",
recursive: bool = False,
quiet: bool = False,
return_gdf: bool = False,
**kwargs,
):
"""
Merge vector files into a single GeoDataFrame.
Args:
files: A string or a list of file paths to be merged.
output: The file path to save the merged GeoDataFrame.
crs: Optional. The coordinate reference system (CRS) of the output GeoDataFrame.
ext: Optional. The file extension of the input files. Default is 'geojson'.
recursive: Optional. If True, search for files recursively in subdirectories. Default is False.
quiet: Optional. If True, suppresses progress messages. Default is False.
return_gdf: Optional. If True, returns the merged GeoDataFrame. Default is False.
**kwargs: Additional keyword arguments to be passed to the `gpd.read_file` function.
Returns:
If `return_gdf` is True, returns the merged GeoDataFrame. Otherwise, returns None.
Raises:
TypeError: If `files` is not a list of file paths.
"""
import pandas as pd
import geopandas as gpd
if isinstance(files, str):
files = find_files(files, ext=ext, recursive=recursive)
if not isinstance(files, list):
raise TypeError("files must be a list of file paths")
gdfs = []
for index, filename in enumerate(files):
if not quiet:
print(f"Reading {index+1} of {len(files)}: {filename}")
gdf = gpd.read_file(filename, **kwargs)
if crs is None:
crs = gdf.crs
gdfs.append(gdf)
if not quiet:
print("Merging GeoDataFrames ...")
gdf = gpd.GeoDataFrame(pd.concat(gdfs, ignore_index=True), crs=crs)
if not quiet:
print(f"Saving merged file to {output} ...")
gdf.to_file(output)
print(f"Saved merged file to {output}")
if return_gdf:
return gdf
meters_to_lnglat(x, y)
¶
coordinate conversion between web mercator to lat/lon in decimal degrees
Parameters:
Name | Type | Description | Default |
---|---|---|---|
x |
float |
The x coordinate. |
required |
y |
float |
The y coordinate. |
required |
Returns:
Type | Description |
---|---|
tuple |
A tuple of (longitude, latitude) in decimal degrees. |
Source code in leafmap/common.py
def meters_to_lnglat(x, y):
"""coordinate conversion between web mercator to lat/lon in decimal degrees
Args:
x (float): The x coordinate.
y (float): The y coordinate.
Returns:
tuple: A tuple of (longitude, latitude) in decimal degrees.
"""
import numpy as np
origin_shift = np.pi * 6378137
longitude = (x / origin_shift) * 180.0
latitude = (y / origin_shift) * 180.0
latitude = (
180 / np.pi * (2 * np.arctan(np.exp(latitude * np.pi / 180.0)) - np.pi / 2.0)
)
return (longitude, latitude)
mosaic(images, output, ext='tif', recursive=True, merge_args={}, to_cog=True, verbose=True, **kwargs)
¶
Mosaics a list of images into a single image. Inspired by https://bit.ly/3A6roDK.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
images |
str | list |
An input directory containing images or a list of images. |
required |
output |
str |
The output image filepath. |
required |
ext |
str |
The file extension of the images. Defaults to 'tif'. |
'tif' |
recursive |
bool |
Whether to recursively search for images in the input directory. Defaults to True. |
True |
merge_args |
dict |
A dictionary of arguments to pass to the rasterio.merge function. Defaults to {}. |
{} |
to_cog |
bool |
Whether to convert the output image to a Cloud Optimized GeoTIFF. Defaults to True. |
True |
verbose |
bool |
Whether to print progress. Defaults to True. |
True |
Source code in leafmap/common.py
def mosaic(
images,
output,
ext="tif",
recursive=True,
merge_args={},
to_cog=True,
verbose=True,
**kwargs,
):
"""Mosaics a list of images into a single image. Inspired by https://bit.ly/3A6roDK.
Args:
images (str | list): An input directory containing images or a list of images.
output (str): The output image filepath.
ext (str, optional): The file extension of the images. Defaults to 'tif'.
recursive (bool, optional): Whether to recursively search for images in the input directory. Defaults to True.
merge_args (dict, optional): A dictionary of arguments to pass to the rasterio.merge function. Defaults to {}.
to_cog (bool, optional): Whether to convert the output image to a Cloud Optimized GeoTIFF. Defaults to True.
verbose (bool, optional): Whether to print progress. Defaults to True.
"""
from rasterio.merge import merge
import rasterio as rio
from pathlib import Path
output = os.path.abspath(output)
if isinstance(images, str):
raster_files = find_files(images, ext=ext, recursive=recursive)
elif isinstance(images, list):
raster_files = images
else:
raise ValueError("images must be a list of raster files.")
raster_to_mosiac = []
if not os.path.exists(os.path.dirname(output)):
os.makedirs(os.path.dirname(output))
for index, p in enumerate(raster_files):
if verbose:
print(f"Reading {index+1}/{len(raster_files)}: {os.path.basename(p)}")
raster = rio.open(p, **kwargs)
raster_to_mosiac.append(raster)
if verbose:
print("Merging rasters...")
arr, transform = merge(raster_to_mosiac, **merge_args)
output_meta = raster.meta.copy()
output_meta.update(
{
"driver": "GTiff",
"height": arr.shape[1],
"width": arr.shape[2],
"transform": transform,
}
)
with rio.open(output, "w", **output_meta) as m:
m.write(arr)
if to_cog:
if verbose:
print("Converting to COG...")
image_to_cog(output, output)
if verbose:
print(f"Saved mosaic to {output}")
mosaic_bounds(url, titiler_endpoint=None, **kwargs)
¶
Get the bounding box of a MosaicJSON.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
HTTP URL to a MosaicJSON. |
required |
titiler_endpoint |
str |
Titiler endpoint, e.g., "https://titiler.xyz". Defaults to None. |
None |
Returns:
Type | Description |
---|---|
list |
A list of values representing [left, bottom, right, top] |
Source code in leafmap/common.py
def mosaic_bounds(url, titiler_endpoint=None, **kwargs):
"""Get the bounding box of a MosaicJSON.
Args:
url (str): HTTP URL to a MosaicJSON.
titiler_endpoint (str, optional): Titiler endpoint, e.g., "https://titiler.xyz". Defaults to None.
Returns:
list: A list of values representing [left, bottom, right, top]
"""
titiler_endpoint = check_titiler_endpoint(titiler_endpoint)
if isinstance(url, str) and url.startswith("http"):
kwargs["url"] = url
else:
raise ValueError("url must be a string and start with http.")
if isinstance(titiler_endpoint, str):
r = requests.get(
f"{titiler_endpoint}/mosaicjson/bounds",
params=kwargs,
).json()
else:
raise ValueError("titiler_endpoint must be a string.")
return r["bounds"]
mosaic_info(url, titiler_endpoint=None, **kwargs)
¶
Get the info of a MosaicJSON.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
HTTP URL to a MosaicJSON. |
required |
titiler_endpoint |
str |
Titiler endpoint, e.g., "https://titiler.xyz". Defaults to None. |
None |
Returns:
Type | Description |
---|---|
dict |
A dictionary containing bounds, center, minzoom, maxzoom, and name as keys. |
Source code in leafmap/common.py
def mosaic_info(url, titiler_endpoint=None, **kwargs):
"""Get the info of a MosaicJSON.
Args:
url (str): HTTP URL to a MosaicJSON.
titiler_endpoint (str, optional): Titiler endpoint, e.g., "https://titiler.xyz". Defaults to None.
Returns:
dict: A dictionary containing bounds, center, minzoom, maxzoom, and name as keys.
"""
titiler_endpoint = check_titiler_endpoint(titiler_endpoint)
if isinstance(url, str) and url.startswith("http"):
kwargs["url"] = url
else:
raise ValueError("url must be a string and start with http.")
if isinstance(titiler_endpoint, str):
r = requests.get(
f"{titiler_endpoint}/mosaicjson/info",
params=kwargs,
).json()
else:
raise ValueError("titiler_endpoint must be a string.")
return r
mosaic_info_geojson(url, titiler_endpoint=None, **kwargs)
¶
Get the info of a MosaicJSON.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
HTTP URL to a MosaicJSON. |
required |
titiler_endpoint |
str |
Titiler endpoint, e.g., "https://titiler.xyz". Defaults to None. |
None |
Returns:
Type | Description |
---|---|
dict |
A dictionary representing a dict of GeoJSON. |
Source code in leafmap/common.py
def mosaic_info_geojson(url, titiler_endpoint=None, **kwargs):
"""Get the info of a MosaicJSON.
Args:
url (str): HTTP URL to a MosaicJSON.
titiler_endpoint (str, optional): Titiler endpoint, e.g., "https://titiler.xyz". Defaults to None.
Returns:
dict: A dictionary representing a dict of GeoJSON.
"""
titiler_endpoint = check_titiler_endpoint(titiler_endpoint)
if isinstance(url, str) and url.startswith("http"):
kwargs["url"] = url
else:
raise ValueError("url must be a string and start with http.")
if isinstance(titiler_endpoint, str):
r = requests.get(
f"{titiler_endpoint}/mosaicjson/info.geojson",
params=kwargs,
).json()
else:
raise ValueError("titiler_endpoint must be a string.")
return r
mosaic_tile(url, titiler_endpoint=None, **kwargs)
¶
Get the tile URL from a MosaicJSON.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
HTTP URL to a MosaicJSON. |
required |
titiler_endpoint |
str |
Titiler endpoint, e.g., "https://titiler.xyz". Defaults to None. |
None |
Returns:
Type | Description |
---|---|
str |
The tile URL. |
Source code in leafmap/common.py
def mosaic_tile(url, titiler_endpoint=None, **kwargs):
"""Get the tile URL from a MosaicJSON.
Args:
url (str): HTTP URL to a MosaicJSON.
titiler_endpoint (str, optional): Titiler endpoint, e.g., "https://titiler.xyz". Defaults to None.
Returns:
str: The tile URL.
"""
titiler_endpoint = check_titiler_endpoint(titiler_endpoint)
if isinstance(url, str) and url.startswith("http"):
kwargs["url"] = url
else:
raise ValueError("url must be a string and start with http.")
if isinstance(titiler_endpoint, str):
r = requests.get(
f"{titiler_endpoint}/mosaicjson/tilejson.json",
params=kwargs,
).json()
else:
raise ValueError("titiler_endpoint must be a string.")
return r["tiles"][0]
nasa_data_download(granules, out_dir=None, provider=None, threads=8)
¶
Downloads NASA Earthdata granules.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
granules |
List[dict] |
The granules to download. |
required |
out_dir |
str |
The output directory where the granules will be downloaded. Defaults to None (current directory). |
None |
provider |
str |
The provider of the granules. |
None |
threads |
int |
The number of threads to use for downloading. Defaults to 8. |
8 |
Source code in leafmap/common.py
def nasa_data_download(
granules: List[dict],
out_dir: Optional[str] = None,
provider: Optional[str] = None,
threads: int = 8,
) -> None:
"""Downloads NASA Earthdata granules.
Args:
granules (List[dict]): The granules to download.
out_dir (str, optional): The output directory where the granules will be downloaded. Defaults to None (current directory).
provider (str, optional): The provider of the granules.
threads (int, optional): The number of threads to use for downloading. Defaults to 8.
"""
import earthaccess
if os.environ.get("USE_MKDOCS") is not None:
return
earthaccess.download(
granules, local_path=out_dir, provider=provider, threads=threads
)
nasa_data_granules_to_gdf(granules, crs='EPSG:4326', output=None, **kwargs)
¶
Converts granules data to a GeoDataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
granules |
List[dict] |
A list of granules. |
required |
crs |
str |
The coordinate reference system (CRS) of the GeoDataFrame. Defaults to "EPSG:4326". |
'EPSG:4326' |
output |
str |
The output file path to save the GeoDataFrame as a file. Defaults to None. |
None |
**kwargs |
Additional keyword arguments for the gpd.GeoDataFrame.to_file() function. |
{} |
Returns:
Type | Description |
---|---|
gpd.GeoDataFrame |
The resulting GeoDataFrame. |
Source code in leafmap/common.py
def nasa_data_granules_to_gdf(
granules: List[dict], crs: str = "EPSG:4326", output: str = None, **kwargs
):
"""Converts granules data to a GeoDataFrame.
Args:
granules (List[dict]): A list of granules.
crs (str, optional): The coordinate reference system (CRS) of the GeoDataFrame. Defaults to "EPSG:4326".
output (str, optional): The output file path to save the GeoDataFrame as a file. Defaults to None.
**kwargs: Additional keyword arguments for the gpd.GeoDataFrame.to_file() function.
Returns:
gpd.GeoDataFrame: The resulting GeoDataFrame.
"""
import pandas as pd
import geopandas as gpd
from shapely.geometry import box, Polygon
df = pd.json_normalize([dict(i.items()) for i in granules])
df.columns = [col.split(".")[-1] for col in df.columns]
df = df.drop("Version", axis=1)
def get_bbox(rectangles):
xmin = min(rectangle["WestBoundingCoordinate"] for rectangle in rectangles)
ymin = min(rectangle["SouthBoundingCoordinate"] for rectangle in rectangles)
xmax = max(rectangle["EastBoundingCoordinate"] for rectangle in rectangles)
ymax = max(rectangle["NorthBoundingCoordinate"] for rectangle in rectangles)
bbox = (xmin, ymin, xmax, ymax)
return bbox
def get_polygon(coordinates):
# Extract the points from the dictionary
points = [
(point["Longitude"], point["Latitude"])
for point in coordinates[0]["Boundary"]["Points"]
]
# Create a Polygon
polygon = Polygon(points)
return polygon
if "BoundingRectangles" in df.columns:
df["bbox"] = df["BoundingRectangles"].apply(get_bbox)
df["geometry"] = df["bbox"].apply(lambda x: box(*x))
elif "GPolygons" in df.columns:
df["geometry"] = df["GPolygons"].apply(get_polygon)
gdf = gpd.GeoDataFrame(df, geometry="geometry")
gdf.crs = crs
if output is not None:
for column in gdf.columns:
if gdf[column].apply(lambda x: isinstance(x, list)).any():
gdf[column] = gdf[column].apply(lambda x: str(x))
gdf.to_file(output, **kwargs)
return gdf
nasa_data_login(strategy='all', persist=False, **kwargs)
¶
Logs in to NASA Earthdata.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
strategy |
str |
The authentication method. "all": (default) try all methods until one works "interactive": enter username and password. "netrc": retrieve username and password from ~/.netrc. "environment": retrieve username and password from $EARTHDATA_USERNAME and $EARTHDATA_PASSWORD. persist (bool, optional): Whether to persist credentials in a .netrc file. Defaults to False. |
'all' |
**kwargs |
Additional keyword arguments for the earthaccess.login() function. |
{} |
Source code in leafmap/common.py
def nasa_data_login(strategy: str = "all", persist: bool = False, **kwargs) -> None:
"""Logs in to NASA Earthdata.
Args:
strategy (str, optional): The authentication method.
"all": (default) try all methods until one works
"interactive": enter username and password.
"netrc": retrieve username and password from ~/.netrc.
"environment": retrieve username and password from $EARTHDATA_USERNAME and $EARTHDATA_PASSWORD.
persist (bool, optional): Whether to persist credentials in a .netrc file. Defaults to False.
**kwargs: Additional keyword arguments for the earthaccess.login() function.
"""
try:
import earthaccess
except ImportError:
install_package("earthaccess")
import earthaccess
try:
earthaccess.login(strategy=strategy, persist=persist, **kwargs)
except:
print(
"Please login to Earthdata first. Register at https://urs.earthdata.nasa.gov"
)
nasa_data_search(count=-1, short_name=None, bbox=None, temporal=None, version=None, doi=None, daac=None, provider=None, output=None, crs='EPSG:4326', return_gdf=False, **kwargs)
¶
Searches for NASA Earthdata granules.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
count |
int |
The number of granules to retrieve. Defaults to -1 (retrieve all). |
-1 |
short_name |
str |
The short name of the dataset. |
None |
bbox |
List[float] |
The bounding box coordinates [xmin, ymin, xmax, ymax]. |
None |
temporal |
str |
The temporal extent of the data. |
None |
version |
str |
The version of the dataset. |
None |
doi |
str |
The Digital Object Identifier (DOI) of the dataset. |
None |
daac |
str |
The Distributed Active Archive Center (DAAC) of the dataset. |
None |
provider |
str |
The provider of the dataset. |
None |
output |
str |
The output file path to save the GeoDataFrame as a file. |
None |
crs |
str |
The coordinate reference system (CRS) of the GeoDataFrame. Defaults to "EPSG:4326". |
'EPSG:4326' |
return_gdf |
bool |
Whether to return the GeoDataFrame in addition to the granules. Defaults to False. |
False |
**kwargs |
Additional keyword arguments for the earthaccess.search_data() function. |
{} |
Returns:
Type | Description |
---|---|
Union[List[dict], tuple] |
The retrieved granules. If return_gdf is True, also returns the resulting GeoDataFrame. |
Source code in leafmap/common.py
def nasa_data_search(
count: int = -1,
short_name: Optional[str] = None,
bbox: Optional[List[float]] = None,
temporal: Optional[str] = None,
version: Optional[str] = None,
doi: Optional[str] = None,
daac: Optional[str] = None,
provider: Optional[str] = None,
output: Optional[str] = None,
crs: str = "EPSG:4326",
return_gdf: bool = False,
**kwargs,
) -> Union[List[dict], tuple]:
"""Searches for NASA Earthdata granules.
Args:
count (int, optional): The number of granules to retrieve. Defaults to -1 (retrieve all).
short_name (str, optional): The short name of the dataset.
bbox (List[float], optional): The bounding box coordinates [xmin, ymin, xmax, ymax].
temporal (str, optional): The temporal extent of the data.
version (str, optional): The version of the dataset.
doi (str, optional): The Digital Object Identifier (DOI) of the dataset.
daac (str, optional): The Distributed Active Archive Center (DAAC) of the dataset.
provider (str, optional): The provider of the dataset.
output (str, optional): The output file path to save the GeoDataFrame as a file.
crs (str, optional): The coordinate reference system (CRS) of the GeoDataFrame. Defaults to "EPSG:4326".
return_gdf (bool, optional): Whether to return the GeoDataFrame in addition to the granules. Defaults to False.
**kwargs: Additional keyword arguments for the earthaccess.search_data() function.
Returns:
Union[List[dict], tuple]: The retrieved granules. If return_gdf is True, also returns the resulting GeoDataFrame.
"""
import earthaccess
if short_name is not None:
kwargs["short_name"] = short_name
if bbox is not None:
kwargs["bounding_box"] = bbox
if temporal is not None:
kwargs["temporal"] = temporal
if version is not None:
kwargs["version"] = version
if doi is not None:
kwargs["doi"] = doi
if daac is not None:
kwargs["daac"] = daac
if provider is not None:
kwargs["provider"] = provider
granules = earthaccess.search_data(
count=count,
**kwargs,
)
if output is not None:
nasa_data_granules_to_gdf(granules, crs=crs, output=output)
if return_gdf:
gdf = nasa_data_granules_to_gdf(granules, crs=crs)
return granules, gdf
else:
return granules
nasa_datasets(keyword=None, df=None, return_short_name=False)
¶
Searches for NASA datasets based on a keyword in a DataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
keyword |
str |
The keyword to search for. Defaults to None. |
None |
df |
pd.DataFrame |
The DataFrame to search in. If None, it will download the NASA dataset CSV from GitHub. Defaults to None. |
None |
return_short_name |
bool |
If True, only returns the list of short names of the matched datasets. Defaults to False. |
False |
Returns:
Type | Description |
---|---|
Union[pd.DataFrame, List[str]] |
Filtered DataFrame if return_short_name is False, otherwise a list of short names. |
Source code in leafmap/common.py
def nasa_datasets(keyword=None, df=None, return_short_name=False):
"""
Searches for NASA datasets based on a keyword in a DataFrame.
Args:
keyword (str, optional): The keyword to search for. Defaults to None.
df (pd.DataFrame, optional): The DataFrame to search in. If None, it will download the NASA dataset CSV from GitHub. Defaults to None.
return_short_name (bool, optional): If True, only returns the list of short names of the matched datasets. Defaults to False.
Returns:
Union[pd.DataFrame, List[str]]: Filtered DataFrame if return_short_name is False, otherwise a list of short names.
"""
import pandas as pd
if df is None:
url = "https://github.com/opengeos/NASA-Earth-Data/raw/main/nasa_earth_data.tsv"
df = pd.read_csv(url, sep="\t")
if keyword is not None:
# Convert keyword and DataFrame values to lowercase
keyword_lower = keyword.lower()
df_lower = df.applymap(lambda x: x.lower() if isinstance(x, str) else x)
# Use boolean indexing to filter the DataFrame
filtered_df = df[
df_lower.astype(str).apply(lambda x: keyword_lower in " ".join(x), axis=1)
].reset_index(drop=True)
if return_short_name:
return filtered_df["ShortName"].tolist()
else:
return filtered_df
else:
if return_short_name:
return df["ShortName"].tolist()
else:
return df
netcdf_tile_layer(filename, variables=None, colormap=None, vmin=None, vmax=None, nodata=None, port='default', debug=False, attribution=None, tile_format='ipyleaflet', layer_name='NetCDF layer', return_client=False, shift_lon=True, lat='lat', lon='lon', **kwargs)
¶
Generate an ipyleaflet/folium TileLayer from a netCDF file. If you are using this function in JupyterHub on a remote server (e.g., Binder, Microsoft Planetary Computer), try adding to following two lines to the beginning of the notebook if the raster does not render properly.
1 2 |
|
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filename |
str |
File path or HTTP URL to the netCDF file. |
required |
variables |
int |
The variable/band names to extract data from the netCDF file. Defaults to None. If None, all variables will be extracted. |
None |
port |
str |
The port to use for the server. Defaults to "default". |
'default' |
colormap |
str |
The name of the colormap from |
None |
vmin |
float |
The minimum value to use when colormapping the colormap when plotting a single band. Defaults to None. |
None |
vmax |
float |
The maximum value to use when colormapping the colormap when plotting a single band. Defaults to None. |
None |
nodata |
float |
The value from the band to use to interpret as not valid data. Defaults to None. |
None |
debug |
bool |
If True, the server will be started in debug mode. Defaults to False. |
False |
projection |
str |
The projection of the GeoTIFF. Defaults to "EPSG:3857". |
required |
attribution |
str |
Attribution for the source raster. This defaults to a message about it being a local file.. Defaults to None. |
None |
tile_format |
str |
The tile layer format. Can be either ipyleaflet or folium. Defaults to "ipyleaflet". |
'ipyleaflet' |
layer_name |
str |
The layer name to use. Defaults to "NetCDF layer". |
'NetCDF layer' |
return_client |
bool |
If True, the tile client will be returned. Defaults to False. |
False |
shift_lon |
bool |
Flag to shift longitude values from [0, 360] to the range [-180, 180]. Defaults to True. |
True |
lat |
str |
Name of the latitude variable. Defaults to 'lat'. |
'lat' |
lon |
str |
Name of the longitude variable. Defaults to 'lon'. |
'lon' |
Returns:
Type | Description |
---|---|
ipyleaflet.TileLayer | folium.TileLayer |
An ipyleaflet.TileLayer or folium.TileLayer. |
Source code in leafmap/common.py
def netcdf_tile_layer(
filename,
variables=None,
colormap=None,
vmin=None,
vmax=None,
nodata=None,
port="default",
debug=False,
attribution=None,
tile_format="ipyleaflet",
layer_name="NetCDF layer",
return_client=False,
shift_lon=True,
lat="lat",
lon="lon",
**kwargs,
):
"""Generate an ipyleaflet/folium TileLayer from a netCDF file.
If you are using this function in JupyterHub on a remote server (e.g., Binder, Microsoft Planetary Computer),
try adding to following two lines to the beginning of the notebook if the raster does not render properly.
import os
os.environ['LOCALTILESERVER_CLIENT_PREFIX'] = f'{os.environ['JUPYTERHUB_SERVICE_PREFIX'].lstrip('/')}/proxy/{{port}}'
Args:
filename (str): File path or HTTP URL to the netCDF file.
variables (int, optional): The variable/band names to extract data from the netCDF file. Defaults to None. If None, all variables will be extracted.
port (str, optional): The port to use for the server. Defaults to "default".
colormap (str, optional): The name of the colormap from `matplotlib` to use when plotting a single band. See https://matplotlib.org/stable/gallery/color/colormap_reference.html. Default is greyscale.
vmin (float, optional): The minimum value to use when colormapping the colormap when plotting a single band. Defaults to None.
vmax (float, optional): The maximum value to use when colormapping the colormap when plotting a single band. Defaults to None.
nodata (float, optional): The value from the band to use to interpret as not valid data. Defaults to None.
debug (bool, optional): If True, the server will be started in debug mode. Defaults to False.
projection (str, optional): The projection of the GeoTIFF. Defaults to "EPSG:3857".
attribution (str, optional): Attribution for the source raster. This defaults to a message about it being a local file.. Defaults to None.
tile_format (str, optional): The tile layer format. Can be either ipyleaflet or folium. Defaults to "ipyleaflet".
layer_name (str, optional): The layer name to use. Defaults to "NetCDF layer".
return_client (bool, optional): If True, the tile client will be returned. Defaults to False.
shift_lon (bool, optional): Flag to shift longitude values from [0, 360] to the range [-180, 180]. Defaults to True.
lat (str, optional): Name of the latitude variable. Defaults to 'lat'.
lon (str, optional): Name of the longitude variable. Defaults to 'lon'.
Returns:
ipyleaflet.TileLayer | folium.TileLayer: An ipyleaflet.TileLayer or folium.TileLayer.
"""
check_package(
"localtileserver", URL="https://github.com/banesullivan/localtileserver"
)
try:
import xarray as xr
except ImportError as e:
raise ImportError(e)
if filename.startswith("http"):
filename = download_file(filename)
if not os.path.exists(filename):
raise FileNotFoundError(f"{filename} does not exist.")
output = filename.replace(".nc", ".tif")
xds = xr.open_dataset(filename, **kwargs)
if shift_lon:
xds.coords[lon] = (xds.coords[lon] + 180) % 360 - 180
xds = xds.sortby(xds.lon)
allowed_vars = list(xds.data_vars.keys())
if isinstance(variables, str):
if variables not in allowed_vars:
raise ValueError(f"{variables} is not a subset of {allowed_vars}.")
variables = [variables]
if variables is not None and len(variables) > 3:
raise ValueError("Only 3 variables can be plotted at a time.")
if variables is not None and (not set(variables).issubset(allowed_vars)):
raise ValueError(f"{variables} must be a subset of {allowed_vars}.")
xds.rio.set_spatial_dims(x_dim=lon, y_dim=lat).rio.to_raster(output)
if variables is None:
if len(allowed_vars) >= 3:
band_idx = [1, 2, 3]
else:
band_idx = [1]
else:
band_idx = [allowed_vars.index(var) + 1 for var in variables]
tile_layer = get_local_tile_layer(
output,
port=port,
debug=debug,
indexes=band_idx,
colormap=colormap,
vmin=vmin,
vmax=vmax,
nodata=nodata,
attribution=attribution,
tile_format=tile_format,
layer_name=layer_name,
return_client=return_client,
)
return tile_layer
netcdf_to_tif(filename, output=None, variables=None, shift_lon=True, lat='lat', lon='lon', lev='lev', level_index=0, time=0, crs='epsg:4326', return_vars=False, **kwargs)
¶
Convert a netcdf file to a GeoTIFF file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filename |
str |
Path to the netcdf file. |
required |
output |
str |
Path to the output GeoTIFF file. Defaults to None. If None, the output file will be the same as the input file with the extension changed to .tif. |
None |
variables |
str | list |
Name of the variable or a list of variables to extract. Defaults to None. If None, all variables will be extracted. |
None |
shift_lon |
bool |
Flag to shift longitude values from [0, 360] to the range [-180, 180]. Defaults to True. |
True |
lat |
str |
Name of the latitude variable. Defaults to 'lat'. |
'lat' |
lon |
str |
Name of the longitude variable. Defaults to 'lon'. |
'lon' |
lev |
str |
Name of the level variable. Defaults to 'lev'. |
'lev' |
level_index |
int |
Index of the level dimension. Defaults to 0'. |
0 |
time |
int |
Index of the time dimension. Defaults to 0'. |
0 |
crs |
str |
The coordinate reference system. Defaults to 'epsg:4326'. |
'epsg:4326' |
return_vars |
bool |
Flag to return all variables. Defaults to False. |
False |
Exceptions:
Type | Description |
---|---|
ImportError |
If the xarray or rioxarray package is not installed. |
FileNotFoundError |
If the netcdf file is not found. |
ValueError |
If the variable is not found in the netcdf file. |
Source code in leafmap/common.py
def netcdf_to_tif(
filename,
output=None,
variables=None,
shift_lon=True,
lat="lat",
lon="lon",
lev="lev",
level_index=0,
time=0,
crs="epsg:4326",
return_vars=False,
**kwargs,
):
"""Convert a netcdf file to a GeoTIFF file.
Args:
filename (str): Path to the netcdf file.
output (str, optional): Path to the output GeoTIFF file. Defaults to None. If None, the output file will be the same as the input file with the extension changed to .tif.
variables (str | list, optional): Name of the variable or a list of variables to extract. Defaults to None. If None, all variables will be extracted.
shift_lon (bool, optional): Flag to shift longitude values from [0, 360] to the range [-180, 180]. Defaults to True.
lat (str, optional): Name of the latitude variable. Defaults to 'lat'.
lon (str, optional): Name of the longitude variable. Defaults to 'lon'.
lev (str, optional): Name of the level variable. Defaults to 'lev'.
level_index (int, optional): Index of the level dimension. Defaults to 0'.
time (int, optional): Index of the time dimension. Defaults to 0'.
crs (str, optional): The coordinate reference system. Defaults to 'epsg:4326'.
return_vars (bool, optional): Flag to return all variables. Defaults to False.
Raises:
ImportError: If the xarray or rioxarray package is not installed.
FileNotFoundError: If the netcdf file is not found.
ValueError: If the variable is not found in the netcdf file.
"""
try:
import xarray as xr
except ImportError as e:
raise ImportError(e)
if filename.startswith("http"):
filename = download_file(filename)
if not os.path.exists(filename):
raise FileNotFoundError(f"{filename} does not exist.")
if output is None:
ext = os.path.splitext(filename)[1].lower()
if ext not in [".nc", ".nc4"]:
raise TypeError(
"The output file must be a netCDF with extension .nc or .nc4."
)
output = filename.replace(ext, ".tif")
else:
output = check_file_path(output)
xds = xr.open_dataset(filename, **kwargs)
coords = list(xds.coords.keys())
if "time" in coords:
xds = xds.isel(time=time, drop=True)
if lev in coords:
xds = xds.isel(lev=level_index, drop=True)
if shift_lon:
xds.coords[lon] = (xds.coords[lon] + 180) % 360 - 180
xds = xds.sortby(xds[lon])
allowed_vars = list(xds.data_vars.keys())
if isinstance(variables, str):
if variables not in allowed_vars:
raise ValueError(f"{variables} is not a valid variable.")
variables = [variables]
if variables is not None and (not set(variables).issubset(allowed_vars)):
raise ValueError(f"{variables} must be a subset of {allowed_vars}.")
if variables is None:
xds.rio.set_spatial_dims(x_dim=lon, y_dim=lat).rio.write_crs(crs).rio.to_raster(
output
)
else:
xds[variables].rio.set_spatial_dims(x_dim=lon, y_dim=lat).rio.write_crs(
crs
).rio.to_raster(output)
if return_vars:
return output, allowed_vars
else:
return output
numpy_to_cog(np_array, out_cog, bounds=None, profile=None, dtype=None, dst_crs=None, coord_crs=None)
¶
Converts a numpy array to a COG file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
np_array |
np.array |
A numpy array representing an image or an HTTP URL to an image. |
required |
out_cog |
str |
The output COG file path. |
required |
bounds |
tuple |
The bounds of the image in the format of (minx, miny, maxx, maxy). Defaults to None. |
None |
profile |
str | dict |
File path to an existing COG file or a dictionary representing the profile. Defaults to None. |
None |
dtype |
str |
The data type of the output COG file. Defaults to None. |
None |
dst_crs |
str |
The coordinate reference system of the output COG file. Defaults to "epsg:4326". |
None |
coord_crs |
str |
The coordinate reference system of bbox coordinates. Defaults to None. |
None |
Source code in leafmap/common.py
def numpy_to_cog(
np_array,
out_cog,
bounds=None,
profile=None,
dtype=None,
dst_crs=None,
coord_crs=None,
):
"""Converts a numpy array to a COG file.
Args:
np_array (np.array): A numpy array representing an image or an HTTP URL to an image.
out_cog (str): The output COG file path.
bounds (tuple, optional): The bounds of the image in the format of (minx, miny, maxx, maxy). Defaults to None.
profile (str | dict, optional): File path to an existing COG file or a dictionary representing the profile. Defaults to None.
dtype (str, optional): The data type of the output COG file. Defaults to None.
dst_crs (str, optional): The coordinate reference system of the output COG file. Defaults to "epsg:4326".
coord_crs (str, optional): The coordinate reference system of bbox coordinates. Defaults to None.
"""
import numpy as np
import rasterio
from rasterio.io import MemoryFile
from rasterio.transform import from_bounds
from rio_cogeo.cogeo import cog_translate
from rio_cogeo.profiles import cog_profiles
warnings.filterwarnings("ignore")
if isinstance(np_array, str):
with rasterio.open(np_array, "r") as ds:
np_array = ds.read()
if not isinstance(np_array, np.ndarray):
raise TypeError("The input array must be a numpy array.")
out_dir = os.path.dirname(out_cog)
check_dir(out_dir)
if profile is not None:
if isinstance(profile, str):
if (not profile.startswith("http")) and (not os.path.exists(profile)):
raise FileNotFoundError("The provided file could not be found.")
with rasterio.open(profile) as ds:
dst_crs = ds.crs
if bounds is None:
bounds = ds.bounds
elif isinstance(profile, rasterio.profiles.Profile):
profile = dict(profile)
elif not isinstance(profile, dict):
raise TypeError("The provided profile must be a file path or a dictionary.")
if bounds is None:
print(
"warning: bounds is not set. Using the default bounds (-180.0, -85.0511, 180.0, 85.0511)"
)
bounds = (-180.0, -85.0511287798066, 180.0, 85.0511287798066)
if not isinstance(bounds, tuple) and len(bounds) != 4:
raise TypeError("The provided bounds must be a tuple of length 4.")
# Rasterio uses numpy array of shape of `(bands, height, width)`
if len(np_array.shape) == 3:
nbands = np_array.shape[0]
height = np_array.shape[1]
width = np_array.shape[2]
elif len(np_array.shape) == 2:
nbands = 1
height = np_array.shape[0]
width = np_array.shape[1]
np_array = np_array.reshape((1, height, width))
else:
raise ValueError("The input array must be a 2D or 3D numpy array.")
if coord_crs is not None and dst_crs is not None:
bounds = transform_bbox_coords(bounds, coord_crs, dst_crs)
src_transform = from_bounds(*bounds, width=width, height=height)
if dtype is None:
dtype = str(np_array.dtype)
if dst_crs is None:
dst_crs = "epsg:4326"
if isinstance(profile, dict):
src_profile = profile
src_profile["count"] = nbands
else:
src_profile = dict(
driver="GTiff",
dtype=dtype,
count=nbands,
height=height,
width=width,
crs=dst_crs,
transform=src_transform,
)
with MemoryFile() as memfile:
with memfile.open(**src_profile) as mem:
# Populate the input file with numpy array
mem.write(np_array)
dst_profile = cog_profiles.get("deflate")
cog_translate(
mem,
out_cog,
dst_profile,
in_memory=True,
quiet=True,
)
numpy_to_image(np_array, filename, transpose=True, bands=None, size=None, resize_args=None, **kwargs)
¶
Converts a numpy array to an image in the specified format, such as JPG, PNG, TIFF, etc.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
np_array |
np.ndarray |
A numpy array or a path to a raster file. |
required |
filename |
str |
The output filename. |
required |
transpose |
bool |
Whether to transpose the array from (bands, rows, cols) to (rows, cols, bands). Defaults to True. |
True |
bands |
int | list |
The band(s) to use, starting from 0. Defaults to None. |
None |
Source code in leafmap/common.py
def numpy_to_image(
np_array,
filename: str,
transpose: bool = True,
bands: Union[int, list] = None,
size: Tuple = None,
resize_args: dict = None,
**kwargs,
) -> None:
"""Converts a numpy array to an image in the specified format, such as JPG, PNG, TIFF, etc.
Args:
np_array (np.ndarray): A numpy array or a path to a raster file.
filename (str): The output filename.
transpose (bool, optional): Whether to transpose the array from (bands, rows, cols) to (rows, cols, bands). Defaults to True.
bands (int | list, optional): The band(s) to use, starting from 0. Defaults to None.
"""
import numpy as np
from PIL import Image
warnings.filterwarnings("ignore")
if isinstance(np_array, str):
np_array = image_to_numpy(np_array)
if not isinstance(np_array, np.ndarray):
raise TypeError("The provided input must be a numpy array.")
if np_array.dtype == np.float64 or np_array.dtype == np.float32:
# Convert the array to uint8
# np_array = (np_array * 255).astype(np.uint8)
np.interp(np_array, (np_array.min(), np_array.max()), (0, 255)).astype(np.uint8)
else:
# The array is already uint8
np_array = np_array
if np_array.ndim == 2:
img = Image.fromarray(np_array)
elif np_array.ndim == 3:
if transpose:
np_array = np_array.transpose(1, 2, 0)
if bands is None:
if np_array.shape[2] < 3:
np_array = np_array[:, :, 0]
elif np_array.shape[2] > 3:
np_array = np_array[:, :, :3]
elif isinstance(bands, list):
if len(bands) == 1:
np_array = np_array[:, :, bands[0]]
else:
np_array = np_array[:, :, bands]
elif isinstance(bands, int):
np_array = np_array[:, :, bands]
img = Image.fromarray(np_array)
else:
raise ValueError("The provided input must be a 2D or 3D numpy array.")
if isinstance(size, tuple):
try:
from skimage.transform import resize
except ImportError:
raise ImportError(
"The scikit-image package is not installed. Please install it with `pip install scikit-image` \
or `conda install scikit-image -c conda-forge`."
)
if resize_args is None:
resize_args = {}
if "preserve_range" not in resize_args:
resize_args["preserve_range"] = True
np_array = resize(np_array, size, **resize_args).astype("uint8")
img = Image.fromarray(np_array)
img.save(filename, **kwargs)
open_image_from_url(url)
¶
Loads an image from the specified URL.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
URL of the image. |
required |
Returns:
Type | Description |
---|---|
object |
Image object. |
Source code in leafmap/common.py
def open_image_from_url(url: str):
"""Loads an image from the specified URL.
Args:
url (str): URL of the image.
Returns:
object: Image object.
"""
from PIL import Image
from io import BytesIO
# from urllib.parse import urlparse
try:
response = requests.get(url)
img = Image.open(BytesIO(response.content))
return img
except Exception as e:
print(e)
overlay_images(image1, image2, alpha=0.5, backend='TkAgg', height_ratios=[10, 1], show_args1={}, show_args2={})
¶
Overlays two images using a slider to control the opacity of the top image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image1 |
str | np.ndarray |
The first input image at the bottom represented as a NumPy array or the path to the image. |
required |
image2 |
_type_ |
The second input image on top represented as a NumPy array or the path to the image. |
required |
alpha |
float |
The alpha value of the top image. Defaults to 0.5. |
0.5 |
backend |
str |
The backend of the matplotlib plot. Defaults to "TkAgg". |
'TkAgg' |
height_ratios |
list |
The height ratios of the two subplots. Defaults to [10, 1]. |
[10, 1] |
show_args1 |
dict |
The keyword arguments to pass to the imshow() function for the first image. Defaults to {}. |
{} |
show_args2 |
dict |
The keyword arguments to pass to the imshow() function for the second image. Defaults to {}. |
{} |
Source code in leafmap/common.py
def overlay_images(
image1,
image2,
alpha=0.5,
backend="TkAgg",
height_ratios=[10, 1],
show_args1={},
show_args2={},
):
"""Overlays two images using a slider to control the opacity of the top image.
Args:
image1 (str | np.ndarray): The first input image at the bottom represented as a NumPy array or the path to the image.
image2 (_type_): The second input image on top represented as a NumPy array or the path to the image.
alpha (float, optional): The alpha value of the top image. Defaults to 0.5.
backend (str, optional): The backend of the matplotlib plot. Defaults to "TkAgg".
height_ratios (list, optional): The height ratios of the two subplots. Defaults to [10, 1].
show_args1 (dict, optional): The keyword arguments to pass to the imshow() function for the first image. Defaults to {}.
show_args2 (dict, optional): The keyword arguments to pass to the imshow() function for the second image. Defaults to {}.
"""
import sys
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.widgets as mpwidgets
if "google.colab" in sys.modules:
backend = "inline"
print(
"The TkAgg backend is not supported in Google Colab. The overlay_images function will not work on Colab."
)
return
matplotlib.use(backend)
if isinstance(image1, str):
if image1.startswith("http"):
image1 = download_file(image1)
if not os.path.exists(image1):
raise ValueError(f"Input path {image1} does not exist.")
if isinstance(image2, str):
if image2.startswith("http"):
image2 = download_file(image2)
if not os.path.exists(image2):
raise ValueError(f"Input path {image2} does not exist.")
# Load the two images
x = plt.imread(image1)
y = plt.imread(image2)
# Create the plot
fig, (ax0, ax1) = plt.subplots(2, 1, gridspec_kw={"height_ratios": height_ratios})
img0 = ax0.imshow(x, **show_args1)
img1 = ax0.imshow(y, alpha=alpha, **show_args2)
# Define the update function
def update(value):
img1.set_alpha(value)
fig.canvas.draw_idle()
# Create the slider
slider0 = mpwidgets.Slider(ax=ax1, label="alpha", valmin=0, valmax=1, valinit=alpha)
slider0.on_changed(update)
# Display the plot
plt.show()
pandas_to_geojson(df, coordinates=['lng', 'lat'], geometry_type='Point', properties=None, output=None)
¶
Convert a DataFrame to a GeoJSON format.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
df |
pd.DataFrame |
The input DataFrame containing the data. |
required |
coordinates |
list |
A list of two column names representing the longitude and latitude coordinates. |
['lng', 'lat'] |
geometry_type |
str |
The type of geometry for the GeoJSON features (e.g., "Point", "LineString", "Polygon"). |
'Point' |
properties |
list |
A list of column names to include in the properties of each GeoJSON feature. If None, all columns except the coordinate columns are included. |
None |
output |
str |
The file path to save the GeoJSON output. If None, the GeoJSON is not saved to a file. |
None |
Returns:
Type | Description |
---|---|
dict |
A dictionary representing the GeoJSON object. |
Source code in leafmap/common.py
def pandas_to_geojson(
df,
coordinates=["lng", "lat"],
geometry_type: str = "Point",
properties: list = None,
output: Optional[str] = None,
) -> dict:
"""
Convert a DataFrame to a GeoJSON format.
Args:
df (pd.DataFrame): The input DataFrame containing the data.
coordinates (list): A list of two column names representing the
longitude and latitude coordinates.
geometry_type (str): The type of geometry for the GeoJSON features
(e.g., "Point", "LineString", "Polygon").
properties (list): A list of column names to include in the properties
of each GeoJSON feature. If None, all columns except the coordinate
columns are included.
output (str, optional): The file path to save the GeoJSON output. If None,
the GeoJSON is not saved to a file.
Returns:
dict: A dictionary representing the GeoJSON object.
"""
import pandas as pd
if isinstance(df, str):
if df.endswith(".csv"):
df = pd.read_csv(df)
elif df.endswith(".json"):
df = pd.read_json(df)
else:
raise ValueError("The input file must be a CSV or JSON file.")
geojson = {"type": "FeatureCollection", "features": []}
if properties is None:
properties = [col for col in df.columns if col not in coordinates]
for _, row in df.iterrows():
feature = {
"type": "Feature",
"properties": {},
"geometry": {"type": geometry_type, "coordinates": []},
}
feature["geometry"]["coordinates"] = list(row[coordinates])
for prop in properties:
feature["properties"][prop] = row[prop]
geojson["features"].append(feature)
if output:
with open(output, "w") as f:
json.dump(geojson, f, indent=4)
return geojson
planet_biannual_tiles_tropical(api_key=None, token_name='PLANET_API_KEY', tile_format='ipyleaflet')
¶
Generates Planet bi-annual imagery TileLayer based on an API key. See https://assets.planet.com/docs/NICFI_UserGuidesFAQ.pdf
Parameters:
Name | Type | Description | Default |
---|---|---|---|
api_key |
str |
The Planet API key. Defaults to None. |
None |
token_name |
str |
The environment variable name of the API key. Defaults to "PLANET_API_KEY". |
'PLANET_API_KEY' |
tile_format |
str |
The TileLayer format, can be either ipyleaflet or folium. Defaults to "ipyleaflet". |
'ipyleaflet' |
Exceptions:
Type | Description |
---|---|
ValueError |
If the tile layer format is invalid. |
Returns:
Type | Description |
---|---|
dict |
A dictionary of TileLayer. |
Source code in leafmap/common.py
def planet_biannual_tiles_tropical(
api_key=None, token_name="PLANET_API_KEY", tile_format="ipyleaflet"
):
"""Generates Planet bi-annual imagery TileLayer based on an API key. See https://assets.planet.com/docs/NICFI_UserGuidesFAQ.pdf
Args:
api_key (str, optional): The Planet API key. Defaults to None.
token_name (str, optional): The environment variable name of the API key. Defaults to "PLANET_API_KEY".
tile_format (str, optional): The TileLayer format, can be either ipyleaflet or folium. Defaults to "ipyleaflet".
Raises:
ValueError: If the tile layer format is invalid.
Returns:
dict: A dictionary of TileLayer.
"""
if tile_format not in ["ipyleaflet", "folium"]:
raise ValueError("The tile format must be either ipyleaflet or folium.")
tiles = {}
link = planet_biannual_tropical(api_key, token_name)
for url in link:
index = url.find("20")
name = "Planet_" + url[index : index + 15]
if tile_format == "ipyleaflet":
tile = ipyleaflet.TileLayer(url=url, attribution="Planet", name=name)
else:
tile = folium.TileLayer(
tiles=url,
attr="Planet",
name=name,
overlay=True,
control=True,
)
tiles[name] = tile
return tiles
planet_biannual_tropical(api_key=None, token_name='PLANET_API_KEY')
¶
Generates Planet bi-annual imagery URLs based on an API key. See https://assets.planet.com/docs/NICFI_UserGuidesFAQ.pdf
Parameters:
Name | Type | Description | Default |
---|---|---|---|
api_key |
str |
The Planet API key. Defaults to None. |
None |
token_name |
str |
The environment variable name of the API key. Defaults to "PLANET_API_KEY". |
'PLANET_API_KEY' |
Exceptions:
Type | Description |
---|---|
ValueError |
If the API key could not be found. |
Returns:
Type | Description |
---|---|
list |
A list of tile URLs. |
Source code in leafmap/common.py
def planet_biannual_tropical(api_key=None, token_name="PLANET_API_KEY"):
"""Generates Planet bi-annual imagery URLs based on an API key. See https://assets.planet.com/docs/NICFI_UserGuidesFAQ.pdf
Args:
api_key (str, optional): The Planet API key. Defaults to None.
token_name (str, optional): The environment variable name of the API key. Defaults to "PLANET_API_KEY".
Raises:
ValueError: If the API key could not be found.
Returns:
list: A list of tile URLs.
"""
if api_key is None:
api_key = os.environ.get(token_name)
if api_key is None:
raise ValueError("The Planet API Key must be provided.")
dates = [
"2015-12_2016-05",
"2016-06_2016-11",
"2016-12_2017-05",
"2017-06_2017-11",
"2017-12_2018-05",
"2018-06_2018-11",
"2018-12_2019-05",
"2019-06_2019-11",
"2019-12_2020-05",
"2020-06_2020-08",
]
link = []
prefix = "https://tiles.planet.com/basemaps/v1/planet-tiles/planet_medres_normalized_analytic_"
subfix = "_mosaic/gmap/{z}/{x}/{y}.png?api_key="
for d in dates:
url = f"{prefix}{d}{subfix}{api_key}"
link.append(url)
return link
planet_by_month(year=2016, month=1, api_key=None, token_name='PLANET_API_KEY')
¶
Gets Planet global mosaic tile url by month. To get a Planet API key, see https://developers.planet.com/quickstart/apis/
Parameters:
Name | Type | Description | Default |
---|---|---|---|
year |
int |
The year of Planet global mosaic, must be >=2016. Defaults to 2016. |
2016 |
month |
int |
The month of Planet global mosaic, must be 1-12. Defaults to 1. |
1 |
api_key |
str |
The Planet API key. Defaults to None. |
None |
token_name |
str |
The environment variable name of the API key. Defaults to "PLANET_API_KEY". |
'PLANET_API_KEY' |
Exceptions:
Type | Description |
---|---|
ValueError |
The Planet API key is not provided. |
ValueError |
The year is invalid. |
ValueError |
The month is invalid. |
ValueError |
The month is invalid. |
Returns:
Type | Description |
---|---|
str |
A Planet global mosaic tile url. |
Source code in leafmap/common.py
def planet_by_month(
year=2016,
month=1,
api_key=None,
token_name="PLANET_API_KEY",
):
"""Gets Planet global mosaic tile url by month. To get a Planet API key, see https://developers.planet.com/quickstart/apis/
Args:
year (int, optional): The year of Planet global mosaic, must be >=2016. Defaults to 2016.
month (int, optional): The month of Planet global mosaic, must be 1-12. Defaults to 1.
api_key (str, optional): The Planet API key. Defaults to None.
token_name (str, optional): The environment variable name of the API key. Defaults to "PLANET_API_KEY".
Raises:
ValueError: The Planet API key is not provided.
ValueError: The year is invalid.
ValueError: The month is invalid.
ValueError: The month is invalid.
Returns:
str: A Planet global mosaic tile url.
"""
from datetime import date
if api_key is None:
api_key = os.environ.get(token_name)
if api_key is None:
raise ValueError("The Planet API Key must be provided.")
today = date.today()
year_now = int(today.strftime("%Y"))
month_now = int(today.strftime("%m"))
# quarter_now = (month_now - 1) // 3 + 1
if year > year_now:
raise ValueError(f"Year must be between 2016 and {year_now}.")
elif year == year_now and month >= month_now:
raise ValueError(f"Month must be less than {month_now} for year {year_now}")
if month < 1 or month > 12:
raise ValueError("Month must be between 1 and 12.")
prefix = "https://tiles.planet.com/basemaps/v1/planet-tiles/global_monthly_"
subfix = "_mosaic/gmap/{z}/{x}/{y}.png?api_key="
m_str = str(year) + "_" + str(month).zfill(2)
url = f"{prefix}{m_str}{subfix}{api_key}"
return url
planet_by_quarter(year=2016, quarter=1, api_key=None, token_name='PLANET_API_KEY')
¶
Gets Planet global mosaic tile url by quarter. To get a Planet API key, see https://developers.planet.com/quickstart/apis/
Parameters:
Name | Type | Description | Default |
---|---|---|---|
year |
int |
The year of Planet global mosaic, must be >=2016. Defaults to 2016. |
2016 |
quarter |
int |
The quarter of Planet global mosaic, must be 1-4. Defaults to 1. |
1 |
api_key |
str |
The Planet API key. Defaults to None. |
None |
token_name |
str |
The environment variable name of the API key. Defaults to "PLANET_API_KEY". |
'PLANET_API_KEY' |
Exceptions:
Type | Description |
---|---|
ValueError |
The Planet API key is not provided. |
ValueError |
The year is invalid. |
ValueError |
The quarter is invalid. |
ValueError |
The quarter is invalid. |
Returns:
Type | Description |
---|---|
str |
A Planet global mosaic tile url. |
Source code in leafmap/common.py
def planet_by_quarter(
year=2016,
quarter=1,
api_key=None,
token_name="PLANET_API_KEY",
):
"""Gets Planet global mosaic tile url by quarter. To get a Planet API key, see https://developers.planet.com/quickstart/apis/
Args:
year (int, optional): The year of Planet global mosaic, must be >=2016. Defaults to 2016.
quarter (int, optional): The quarter of Planet global mosaic, must be 1-4. Defaults to 1.
api_key (str, optional): The Planet API key. Defaults to None.
token_name (str, optional): The environment variable name of the API key. Defaults to "PLANET_API_KEY".
Raises:
ValueError: The Planet API key is not provided.
ValueError: The year is invalid.
ValueError: The quarter is invalid.
ValueError: The quarter is invalid.
Returns:
str: A Planet global mosaic tile url.
"""
from datetime import date
if api_key is None:
api_key = os.environ.get(token_name)
if api_key is None:
raise ValueError("The Planet API Key must be provided.")
today = date.today()
year_now = int(today.strftime("%Y"))
month_now = int(today.strftime("%m"))
quarter_now = (month_now - 1) // 3 + 1
if year > year_now:
raise ValueError(f"Year must be between 2016 and {year_now}.")
elif year == year_now and quarter >= quarter_now:
raise ValueError(f"Quarter must be less than {quarter_now} for year {year_now}")
if quarter < 1 or quarter > 4:
raise ValueError("Quarter must be between 1 and 4.")
prefix = "https://tiles.planet.com/basemaps/v1/planet-tiles/global_quarterly_"
subfix = "_mosaic/gmap/{z}/{x}/{y}.png?api_key="
m_str = str(year) + "q" + str(quarter)
url = f"{prefix}{m_str}{subfix}{api_key}"
return url
planet_catalog(api_key=None, token_name='PLANET_API_KEY')
¶
Generates Planet bi-annual and monthly imagery URLs based on an API key. See https://assets.planet.com/docs/NICFI_UserGuidesFAQ.pdf
Parameters:
Name | Type | Description | Default |
---|---|---|---|
api_key |
str |
The Planet API key. Defaults to None. |
None |
token_name |
str |
The environment variable name of the API key. Defaults to "PLANET_API_KEY". |
'PLANET_API_KEY' |
Returns:
Type | Description |
---|---|
list |
A list of tile URLs. |
Source code in leafmap/common.py
def planet_catalog(api_key=None, token_name="PLANET_API_KEY"):
"""Generates Planet bi-annual and monthly imagery URLs based on an API key. See https://assets.planet.com/docs/NICFI_UserGuidesFAQ.pdf
Args:
api_key (str, optional): The Planet API key. Defaults to None.
token_name (str, optional): The environment variable name of the API key. Defaults to "PLANET_API_KEY".
Returns:
list: A list of tile URLs.
"""
quarterly = planet_quarterly(api_key, token_name)
monthly = planet_monthly(api_key, token_name)
return quarterly + monthly
planet_catalog_tropical(api_key=None, token_name='PLANET_API_KEY')
¶
Generates Planet bi-annual and monthly imagery URLs based on an API key. See https://assets.planet.com/docs/NICFI_UserGuidesFAQ.pdf
Parameters:
Name | Type | Description | Default |
---|---|---|---|
api_key |
str |
The Planet API key. Defaults to None. |
None |
token_name |
str |
The environment variable name of the API key. Defaults to "PLANET_API_KEY". |
'PLANET_API_KEY' |
Returns:
Type | Description |
---|---|
list |
A list of tile URLs. |
Source code in leafmap/common.py
def planet_catalog_tropical(api_key=None, token_name="PLANET_API_KEY"):
"""Generates Planet bi-annual and monthly imagery URLs based on an API key. See https://assets.planet.com/docs/NICFI_UserGuidesFAQ.pdf
Args:
api_key (str, optional): The Planet API key. Defaults to None.
token_name (str, optional): The environment variable name of the API key. Defaults to "PLANET_API_KEY".
Returns:
list: A list of tile URLs.
"""
biannual = planet_biannual_tropical(api_key, token_name)
monthly = planet_monthly_tropical(api_key, token_name)
return biannual + monthly
planet_monthly(api_key=None, token_name='PLANET_API_KEY')
¶
Generates Planet monthly imagery URLs based on an API key. To get a Planet API key, see https://developers.planet.com/quickstart/apis/
Parameters:
Name | Type | Description | Default |
---|---|---|---|
api_key |
str |
The Planet API key. Defaults to None. |
None |
token_name |
str |
The environment variable name of the API key. Defaults to "PLANET_API_KEY". |
'PLANET_API_KEY' |
Exceptions:
Type | Description |
---|---|
ValueError |
If the API key could not be found. |
Returns:
Type | Description |
---|---|
list |
A list of tile URLs. |
Source code in leafmap/common.py
def planet_monthly(api_key=None, token_name="PLANET_API_KEY"):
"""Generates Planet monthly imagery URLs based on an API key. To get a Planet API key, see https://developers.planet.com/quickstart/apis/
Args:
api_key (str, optional): The Planet API key. Defaults to None.
token_name (str, optional): The environment variable name of the API key. Defaults to "PLANET_API_KEY".
Raises:
ValueError: If the API key could not be found.
Returns:
list: A list of tile URLs.
"""
from datetime import date
if api_key is None:
api_key = os.environ.get(token_name)
if api_key is None:
raise ValueError("The Planet API Key must be provided.")
today = date.today()
year_now = int(today.strftime("%Y"))
month_now = int(today.strftime("%m"))
link = []
prefix = "https://tiles.planet.com/basemaps/v1/planet-tiles/global_monthly_"
subfix = "_mosaic/gmap/{z}/{x}/{y}.png?api_key="
for year in range(2016, year_now + 1):
for month in range(1, 13):
m_str = str(year) + "_" + str(month).zfill(2)
if year == year_now and month >= month_now:
break
url = f"{prefix}{m_str}{subfix}{api_key}"
link.append(url)
return link
planet_monthly_tiles(api_key=None, token_name='PLANET_API_KEY', tile_format='ipyleaflet')
¶
Generates Planet monthly imagery TileLayer based on an API key. To get a Planet API key, see https://developers.planet.com/quickstart/apis/
Parameters:
Name | Type | Description | Default |
---|---|---|---|
api_key |
str |
The Planet API key. Defaults to None. |
None |
token_name |
str |
The environment variable name of the API key. Defaults to "PLANET_API_KEY". |
'PLANET_API_KEY' |
tile_format |
str |
The TileLayer format, can be either ipyleaflet or folium. Defaults to "ipyleaflet". |
'ipyleaflet' |
Exceptions:
Type | Description |
---|---|
ValueError |
If the tile layer format is invalid. |
Returns:
Type | Description |
---|---|
dict |
A dictionary of TileLayer. |
Source code in leafmap/common.py
def planet_monthly_tiles(
api_key=None, token_name="PLANET_API_KEY", tile_format="ipyleaflet"
):
"""Generates Planet monthly imagery TileLayer based on an API key. To get a Planet API key, see https://developers.planet.com/quickstart/apis/
Args:
api_key (str, optional): The Planet API key. Defaults to None.
token_name (str, optional): The environment variable name of the API key. Defaults to "PLANET_API_KEY".
tile_format (str, optional): The TileLayer format, can be either ipyleaflet or folium. Defaults to "ipyleaflet".
Raises:
ValueError: If the tile layer format is invalid.
Returns:
dict: A dictionary of TileLayer.
"""
if tile_format not in ["ipyleaflet", "folium"]:
raise ValueError("The tile format must be either ipyleaflet or folium.")
tiles = {}
link = planet_monthly(api_key, token_name)
for url in link:
index = url.find("20")
name = "Planet_" + url[index : index + 7]
if tile_format == "ipyleaflet":
tile = ipyleaflet.TileLayer(url=url, attribution="Planet", name=name)
else:
tile = folium.TileLayer(
tiles=url,
attr="Planet",
name=name,
overlay=True,
control=True,
)
tiles[name] = tile
return tiles
planet_monthly_tiles_tropical(api_key=None, token_name='PLANET_API_KEY', tile_format='ipyleaflet')
¶
Generates Planet monthly imagery TileLayer based on an API key. See https://assets.planet.com/docs/NICFI_UserGuidesFAQ.pdf
Parameters:
Name | Type | Description | Default |
---|---|---|---|
api_key |
str |
The Planet API key. Defaults to None. |
None |
token_name |
str |
The environment variable name of the API key. Defaults to "PLANET_API_KEY". |
'PLANET_API_KEY' |
tile_format |
str |
The TileLayer format, can be either ipyleaflet or folium. Defaults to "ipyleaflet". |
'ipyleaflet' |
Exceptions:
Type | Description |
---|---|
ValueError |
If the tile layer format is invalid. |
Returns:
Type | Description |
---|---|
dict |
A dictionary of TileLayer. |
Source code in leafmap/common.py
def planet_monthly_tiles_tropical(
api_key=None, token_name="PLANET_API_KEY", tile_format="ipyleaflet"
):
"""Generates Planet monthly imagery TileLayer based on an API key. See https://assets.planet.com/docs/NICFI_UserGuidesFAQ.pdf
Args:
api_key (str, optional): The Planet API key. Defaults to None.
token_name (str, optional): The environment variable name of the API key. Defaults to "PLANET_API_KEY".
tile_format (str, optional): The TileLayer format, can be either ipyleaflet or folium. Defaults to "ipyleaflet".
Raises:
ValueError: If the tile layer format is invalid.
Returns:
dict: A dictionary of TileLayer.
"""
if tile_format not in ["ipyleaflet", "folium"]:
raise ValueError("The tile format must be either ipyleaflet or folium.")
tiles = {}
link = planet_monthly_tropical(api_key, token_name)
for url in link:
index = url.find("20")
name = "Planet_" + url[index : index + 7]
if tile_format == "ipyleaflet":
tile = ipyleaflet.TileLayer(url=url, attribution="Planet", name=name)
else:
tile = folium.TileLayer(
tiles=url,
attr="Planet",
name=name,
overlay=True,
control=True,
)
tiles[name] = tile
return tiles
planet_monthly_tropical(api_key=None, token_name='PLANET_API_KEY')
¶
Generates Planet monthly imagery URLs based on an API key. See https://assets.planet.com/docs/NICFI_UserGuidesFAQ.pdf
Parameters:
Name | Type | Description | Default |
---|---|---|---|
api_key |
str |
The Planet API key. Defaults to None. |
None |
token_name |
str |
The environment variable name of the API key. Defaults to "PLANET_API_KEY". |
'PLANET_API_KEY' |
Exceptions:
Type | Description |
---|---|
ValueError |
If the API key could not be found. |
Returns:
Type | Description |
---|---|
list |
A list of tile URLs. |
Source code in leafmap/common.py
def planet_monthly_tropical(api_key=None, token_name="PLANET_API_KEY"):
"""Generates Planet monthly imagery URLs based on an API key. See https://assets.planet.com/docs/NICFI_UserGuidesFAQ.pdf
Args:
api_key (str, optional): The Planet API key. Defaults to None.
token_name (str, optional): The environment variable name of the API key. Defaults to "PLANET_API_KEY".
Raises:
ValueError: If the API key could not be found.
Returns:
list: A list of tile URLs.
"""
from datetime import date
if api_key is None:
api_key = os.environ.get(token_name)
if api_key is None:
raise ValueError("The Planet API Key must be provided.")
today = date.today()
year_now = int(today.strftime("%Y"))
month_now = int(today.strftime("%m"))
links = []
prefix = "https://tiles.planet.com/basemaps/v1/planet-tiles/planet_medres_normalized_analytic_"
subfix = "_mosaic/gmap/{z}/{x}/{y}.png?api_key="
for year in range(2020, year_now + 1):
for month in range(1, 13):
m_str = str(year) + "-" + str(month).zfill(2)
if year == 2020 and month < 9:
continue
if year == year_now and month >= month_now:
break
url = f"{prefix}{m_str}{subfix}{api_key}"
links.append(url)
return links
planet_quarterly(api_key=None, token_name='PLANET_API_KEY')
¶
Generates Planet quarterly imagery URLs based on an API key. To get a Planet API key, see https://developers.planet.com/quickstart/apis/
Parameters:
Name | Type | Description | Default |
---|---|---|---|
api_key |
str |
The Planet API key. Defaults to None. |
None |
token_name |
str |
The environment variable name of the API key. Defaults to "PLANET_API_KEY". |
'PLANET_API_KEY' |
Exceptions:
Type | Description |
---|---|
ValueError |
If the API key could not be found. |
Returns:
Type | Description |
---|---|
list |
A list of tile URLs. |
Source code in leafmap/common.py
def planet_quarterly(api_key=None, token_name="PLANET_API_KEY"):
"""Generates Planet quarterly imagery URLs based on an API key. To get a Planet API key, see https://developers.planet.com/quickstart/apis/
Args:
api_key (str, optional): The Planet API key. Defaults to None.
token_name (str, optional): The environment variable name of the API key. Defaults to "PLANET_API_KEY".
Raises:
ValueError: If the API key could not be found.
Returns:
list: A list of tile URLs.
"""
from datetime import date
if api_key is None:
api_key = os.environ.get(token_name)
if api_key is None:
raise ValueError("The Planet API Key must be provided.")
today = date.today()
year_now = int(today.strftime("%Y"))
month_now = int(today.strftime("%m"))
quarter_now = (month_now - 1) // 3 + 1
link = []
prefix = "https://tiles.planet.com/basemaps/v1/planet-tiles/global_quarterly_"
subfix = "_mosaic/gmap/{z}/{x}/{y}.png?api_key="
for year in range(2016, year_now + 1):
for quarter in range(1, 5):
m_str = str(year) + "q" + str(quarter)
if year == year_now and quarter >= quarter_now:
break
url = f"{prefix}{m_str}{subfix}{api_key}"
link.append(url)
return link
planet_quarterly_tiles(api_key=None, token_name='PLANET_API_KEY', tile_format='ipyleaflet')
¶
Generates Planet quarterly imagery TileLayer based on an API key. To get a Planet API key, see https://developers.planet.com/quickstart/apis/
Parameters:
Name | Type | Description | Default |
---|---|---|---|
api_key |
str |
The Planet API key. Defaults to None. |
None |
token_name |
str |
The environment variable name of the API key. Defaults to "PLANET_API_KEY". |
'PLANET_API_KEY' |
tile_format |
str |
The TileLayer format, can be either ipyleaflet or folium. Defaults to "ipyleaflet". |
'ipyleaflet' |
Exceptions:
Type | Description |
---|---|
ValueError |
If the tile layer format is invalid. |
Returns:
Type | Description |
---|---|
dict |
A dictionary of TileLayer. |
Source code in leafmap/common.py
def planet_quarterly_tiles(
api_key=None, token_name="PLANET_API_KEY", tile_format="ipyleaflet"
):
"""Generates Planet quarterly imagery TileLayer based on an API key. To get a Planet API key, see https://developers.planet.com/quickstart/apis/
Args:
api_key (str, optional): The Planet API key. Defaults to None.
token_name (str, optional): The environment variable name of the API key. Defaults to "PLANET_API_KEY".
tile_format (str, optional): The TileLayer format, can be either ipyleaflet or folium. Defaults to "ipyleaflet".
Raises:
ValueError: If the tile layer format is invalid.
Returns:
dict: A dictionary of TileLayer.
"""
if tile_format not in ["ipyleaflet", "folium"]:
raise ValueError("The tile format must be either ipyleaflet or folium.")
tiles = {}
links = planet_quarterly(api_key, token_name)
for url in links:
index = url.find("20")
name = "Planet_" + url[index : index + 6]
if tile_format == "ipyleaflet":
tile = ipyleaflet.TileLayer(url=url, attribution="Planet", name=name)
else:
tile = folium.TileLayer(
tiles=url,
attr="Planet",
name=name,
overlay=True,
control=True,
)
tiles[name] = tile
return tiles
planet_tile_by_month(year=2016, month=1, name=None, api_key=None, token_name='PLANET_API_KEY', tile_format='ipyleaflet')
¶
Generates Planet monthly imagery TileLayer based on an API key. To get a Planet API key, see https://developers.planet.com/quickstart/apis
Parameters:
Name | Type | Description | Default |
---|---|---|---|
year |
int |
The year of Planet global mosaic, must be >=2016. Defaults to 2016. |
2016 |
month |
int |
The month of Planet global mosaic, must be 1-12. Defaults to 1. |
1 |
name |
str |
The layer name to use. Defaults to None. |
None |
api_key |
str |
The Planet API key. Defaults to None. |
None |
token_name |
str |
The environment variable name of the API key. Defaults to "PLANET_API_KEY". |
'PLANET_API_KEY' |
tile_format |
str |
The TileLayer format, can be either ipyleaflet or folium. Defaults to "ipyleaflet". |
'ipyleaflet' |
Exceptions:
Type | Description |
---|---|
ValueError |
If the tile layer format is invalid. |
Returns:
Type | Description |
---|---|
dict |
A dictionary of TileLayer. |
Source code in leafmap/common.py
def planet_tile_by_month(
year=2016,
month=1,
name=None,
api_key=None,
token_name="PLANET_API_KEY",
tile_format="ipyleaflet",
):
"""Generates Planet monthly imagery TileLayer based on an API key. To get a Planet API key, see https://developers.planet.com/quickstart/apis
Args:
year (int, optional): The year of Planet global mosaic, must be >=2016. Defaults to 2016.
month (int, optional): The month of Planet global mosaic, must be 1-12. Defaults to 1.
name (str, optional): The layer name to use. Defaults to None.
api_key (str, optional): The Planet API key. Defaults to None.
token_name (str, optional): The environment variable name of the API key. Defaults to "PLANET_API_KEY".
tile_format (str, optional): The TileLayer format, can be either ipyleaflet or folium. Defaults to "ipyleaflet".
Raises:
ValueError: If the tile layer format is invalid.
Returns:
dict: A dictionary of TileLayer.
"""
if tile_format not in ["ipyleaflet", "folium"]:
raise ValueError("The tile format must be either ipyleaflet or folium.")
url = planet_by_month(year, month, api_key, token_name)
if name is None:
name = "Planet_" + str(year) + "_" + str(month).zfill(2)
if tile_format == "ipyleaflet":
tile = ipyleaflet.TileLayer(url=url, attribution="Planet", name=name)
else:
tile = folium.TileLayer(
tiles=url,
attr="Planet",
name=name,
overlay=True,
control=True,
)
return tile
planet_tile_by_quarter(year=2016, quarter=1, name=None, api_key=None, token_name='PLANET_API_KEY', tile_format='ipyleaflet')
¶
Generates Planet quarterly imagery TileLayer based on an API key. To get a Planet API key, see https://developers.planet.com/quickstart/apis
Parameters:
Name | Type | Description | Default |
---|---|---|---|
year |
int |
The year of Planet global mosaic, must be >=2016. Defaults to 2016. |
2016 |
quarter |
int |
The quarter of Planet global mosaic, must be 1-4. Defaults to 1. |
1 |
name |
str |
The layer name to use. Defaults to None. |
None |
api_key |
str |
The Planet API key. Defaults to None. |
None |
token_name |
str |
The environment variable name of the API key. Defaults to "PLANET_API_KEY". |
'PLANET_API_KEY' |
tile_format |
str |
The TileLayer format, can be either ipyleaflet or folium. Defaults to "ipyleaflet". |
'ipyleaflet' |
Exceptions:
Type | Description |
---|---|
ValueError |
If the tile layer format is invalid. |
Returns:
Type | Description |
---|---|
dict |
A dictionary of TileLayer. |
Source code in leafmap/common.py
def planet_tile_by_quarter(
year=2016,
quarter=1,
name=None,
api_key=None,
token_name="PLANET_API_KEY",
tile_format="ipyleaflet",
):
"""Generates Planet quarterly imagery TileLayer based on an API key. To get a Planet API key, see https://developers.planet.com/quickstart/apis
Args:
year (int, optional): The year of Planet global mosaic, must be >=2016. Defaults to 2016.
quarter (int, optional): The quarter of Planet global mosaic, must be 1-4. Defaults to 1.
name (str, optional): The layer name to use. Defaults to None.
api_key (str, optional): The Planet API key. Defaults to None.
token_name (str, optional): The environment variable name of the API key. Defaults to "PLANET_API_KEY".
tile_format (str, optional): The TileLayer format, can be either ipyleaflet or folium. Defaults to "ipyleaflet".
Raises:
ValueError: If the tile layer format is invalid.
Returns:
dict: A dictionary of TileLayer.
"""
if tile_format not in ["ipyleaflet", "folium"]:
raise ValueError("The tile format must be either ipyleaflet or folium.")
url = planet_by_quarter(year, quarter, api_key, token_name)
if name is None:
name = "Planet_" + str(year) + "_q" + str(quarter)
if tile_format == "ipyleaflet":
tile = ipyleaflet.TileLayer(url=url, attribution="Planet", name=name)
else:
tile = folium.TileLayer(
tiles=url,
attr="Planet",
name=name,
overlay=True,
control=True,
)
return tile
planet_tiles(api_key=None, token_name='PLANET_API_KEY', tile_format='ipyleaflet')
¶
Generates Planet imagery TileLayer based on an API key. To get a Planet API key, see https://developers.planet.com/quickstart/apis/
Parameters:
Name | Type | Description | Default |
---|---|---|---|
api_key |
str |
The Planet API key. Defaults to None. |
None |
token_name |
str |
The environment variable name of the API key. Defaults to "PLANET_API_KEY". |
'PLANET_API_KEY' |
tile_format |
str |
The TileLayer format, can be either ipyleaflet or folium. Defaults to "ipyleaflet". |
'ipyleaflet' |
Exceptions:
Type | Description |
---|---|
ValueError |
If the tile layer format is invalid. |
Returns:
Type | Description |
---|---|
dict |
A dictionary of TileLayer. |
Source code in leafmap/common.py
def planet_tiles(api_key=None, token_name="PLANET_API_KEY", tile_format="ipyleaflet"):
"""Generates Planet imagery TileLayer based on an API key. To get a Planet API key, see https://developers.planet.com/quickstart/apis/
Args:
api_key (str, optional): The Planet API key. Defaults to None.
token_name (str, optional): The environment variable name of the API key. Defaults to "PLANET_API_KEY".
tile_format (str, optional): The TileLayer format, can be either ipyleaflet or folium. Defaults to "ipyleaflet".
Raises:
ValueError: If the tile layer format is invalid.
Returns:
dict: A dictionary of TileLayer.
"""
catalog = {}
quarterly = planet_quarterly_tiles(api_key, token_name, tile_format)
monthly = planet_monthly_tiles(api_key, token_name, tile_format)
for key in quarterly:
catalog[key] = quarterly[key]
for key in monthly:
catalog[key] = monthly[key]
return catalog
planet_tiles_tropical(api_key=None, token_name='PLANET_API_KEY', tile_format='ipyleaflet')
¶
Generates Planet monthly imagery TileLayer based on an API key. See https://assets.planet.com/docs/NICFI_UserGuidesFAQ.pdf
Parameters:
Name | Type | Description | Default |
---|---|---|---|
api_key |
str |
The Planet API key. Defaults to None. |
None |
token_name |
str |
The environment variable name of the API key. Defaults to "PLANET_API_KEY". |
'PLANET_API_KEY' |
tile_format |
str |
The TileLayer format, can be either ipyleaflet or folium. Defaults to "ipyleaflet". |
'ipyleaflet' |
Exceptions:
Type | Description |
---|---|
ValueError |
If the tile layer format is invalid. |
Returns:
Type | Description |
---|---|
dict |
A dictionary of TileLayer. |
Source code in leafmap/common.py
def planet_tiles_tropical(
api_key=None, token_name="PLANET_API_KEY", tile_format="ipyleaflet"
):
"""Generates Planet monthly imagery TileLayer based on an API key. See https://assets.planet.com/docs/NICFI_UserGuidesFAQ.pdf
Args:
api_key (str, optional): The Planet API key. Defaults to None.
token_name (str, optional): The environment variable name of the API key. Defaults to "PLANET_API_KEY".
tile_format (str, optional): The TileLayer format, can be either ipyleaflet or folium. Defaults to "ipyleaflet".
Raises:
ValueError: If the tile layer format is invalid.
Returns:
dict: A dictionary of TileLayer.
"""
catalog = {}
biannul = planet_biannual_tiles_tropical(api_key, token_name, tile_format)
monthly = planet_monthly_tiles_tropical(api_key, token_name, tile_format)
for key in biannul:
catalog[key] = biannul[key]
for key in monthly:
catalog[key] = monthly[key]
return catalog
plot_raster(image, band=None, cmap='terrain', proj='EPSG:3857', figsize=None, open_kwargs={}, **kwargs)
¶
Plot a raster image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
str | xarray.DataArray |
The input raster image, can be a file path, HTTP URL, or xarray.DataArray. |
required |
band |
int |
The band index, starting from zero. Defaults to None. |
None |
cmap |
str |
The matplotlib colormap to use. Defaults to "terrain". |
'terrain' |
proj |
str |
The EPSG projection code. Defaults to "EPSG:3857". |
'EPSG:3857' |
figsize |
tuple |
The figure size as a tuple, such as (10, 8). Defaults to None. |
None |
open_kwargs |
dict |
The keyword arguments to pass to rioxarray.open_rasterio. Defaults to {}. |
{} |
**kwargs |
Additional keyword arguments to pass to xarray.DataArray.plot(). |
{} |
Source code in leafmap/common.py
def plot_raster(
image,
band=None,
cmap="terrain",
proj="EPSG:3857",
figsize=None,
open_kwargs={},
**kwargs,
):
"""Plot a raster image.
Args:
image (str | xarray.DataArray ): The input raster image, can be a file path, HTTP URL, or xarray.DataArray.
band (int, optional): The band index, starting from zero. Defaults to None.
cmap (str, optional): The matplotlib colormap to use. Defaults to "terrain".
proj (str, optional): The EPSG projection code. Defaults to "EPSG:3857".
figsize (tuple, optional): The figure size as a tuple, such as (10, 8). Defaults to None.
open_kwargs (dict, optional): The keyword arguments to pass to rioxarray.open_rasterio. Defaults to {}.
**kwargs: Additional keyword arguments to pass to xarray.DataArray.plot().
"""
if os.environ.get("USE_MKDOCS") is not None:
return
try:
import pvxarray
import rioxarray
import xarray
except ImportError:
print(
"pyxarray and rioxarray are required for plotting. Please install them using 'pip install rioxarray pyvista-xarray'."
)
return
if isinstance(image, str):
da = rioxarray.open_rasterio(image, **open_kwargs)
elif isinstance(image, xarray.DataArray):
da = image
else:
raise ValueError("image must be a string or xarray.Dataset.")
if band is not None:
da = da[dict(band=band)]
da = da.rio.reproject(proj)
kwargs["cmap"] = cmap
kwargs["figsize"] = figsize
da.plot(**kwargs)
plot_raster_3d(image, band=None, cmap='terrain', factor=1.0, proj='EPSG:3857', background=None, x=None, y=None, z=None, order=None, component=None, open_kwargs={}, mesh_kwargs={}, **kwargs)
¶
Plot a raster image in 3D.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
str | xarray.DataArray |
The input raster image, can be a file path, HTTP URL, or xarray.DataArray. |
required |
band |
int |
The band index, starting from zero. Defaults to None. |
None |
cmap |
str |
The matplotlib colormap to use. Defaults to "terrain". |
'terrain' |
factor |
float |
The scaling factor for the raster. Defaults to 1.0. |
1.0 |
proj |
str |
The EPSG projection code. Defaults to "EPSG:3857". |
'EPSG:3857' |
background |
str |
The background color. Defaults to None. |
None |
x |
str |
The x coordinate. Defaults to None. |
None |
y |
str |
The y coordinate. Defaults to None. |
None |
z |
str |
The z coordinate. Defaults to None. |
None |
order |
str |
The order of the coordinates. Defaults to None. |
None |
component |
str |
The component of the coordinates. Defaults to None. |
None |
open_kwargs |
dict |
The keyword arguments to pass to rioxarray.open_rasterio. Defaults to {}. |
{} |
mesh_kwargs |
dict |
The keyword arguments to pass to pyvista.mesh.warp_by_scalar(). Defaults to {}. |
{} |
**kwargs |
Additional keyword arguments to pass to xarray.DataArray.plot(). |
{} |
Source code in leafmap/common.py
def plot_raster_3d(
image,
band=None,
cmap="terrain",
factor=1.0,
proj="EPSG:3857",
background=None,
x=None,
y=None,
z=None,
order=None,
component=None,
open_kwargs={},
mesh_kwargs={},
**kwargs,
):
"""Plot a raster image in 3D.
Args:
image (str | xarray.DataArray): The input raster image, can be a file path, HTTP URL, or xarray.DataArray.
band (int, optional): The band index, starting from zero. Defaults to None.
cmap (str, optional): The matplotlib colormap to use. Defaults to "terrain".
factor (float, optional): The scaling factor for the raster. Defaults to 1.0.
proj (str, optional): The EPSG projection code. Defaults to "EPSG:3857".
background (str, optional): The background color. Defaults to None.
x (str, optional): The x coordinate. Defaults to None.
y (str, optional): The y coordinate. Defaults to None.
z (str, optional): The z coordinate. Defaults to None.
order (str, optional): The order of the coordinates. Defaults to None.
component (str, optional): The component of the coordinates. Defaults to None.
open_kwargs (dict, optional): The keyword arguments to pass to rioxarray.open_rasterio. Defaults to {}.
mesh_kwargs (dict, optional): The keyword arguments to pass to pyvista.mesh.warp_by_scalar(). Defaults to {}.
**kwargs: Additional keyword arguments to pass to xarray.DataArray.plot().
"""
import sys
if os.environ.get("USE_MKDOCS") is not None:
return
if "google.colab" in sys.modules:
print("This function is not supported in Google Colab.")
return
try:
import pvxarray
import pyvista
import rioxarray
import xarray
except ImportError:
print(
"pyxarray and rioxarray are required for plotting. Please install them using 'pip install rioxarray pyvista-xarray'."
)
return
if isinstance(background, str):
pyvista.global_theme.background = background
if isinstance(image, str):
da = rioxarray.open_rasterio(image, **open_kwargs)
elif isinstance(image, xarray.DataArray):
da = image
else:
raise ValueError("image must be a string or xarray.Dataset.")
if band is not None:
da = da[dict(band=band)]
da = da.rio.reproject(proj)
mesh_kwargs["factor"] = factor
kwargs["cmap"] = cmap
coords = list(da.coords)
if x is None:
if "x" in coords:
x = "x"
elif "lon" in coords:
x = "lon"
if y is None:
if "y" in coords:
y = "y"
elif "lat" in coords:
y = "lat"
if z is None:
if "z" in coords:
z = "z"
elif "elevation" in coords:
z = "elevation"
elif "band" in coords:
z = "band"
# Grab the mesh object for use with PyVista
mesh = da.pyvista.mesh(x=x, y=y, z=z, order=order, component=component)
# Warp top and plot in 3D
mesh.warp_by_scalar(**mesh_kwargs).plot(**kwargs)
pmtiles_header(input_file)
¶
Fetch the header information from a local or remote .pmtiles file.
This function retrieves the header from a PMTiles file, either local or hosted remotely. It deserializes the header and calculates the center and bounds of the tiles from the given metadata in the header.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
input_file |
str |
Path to the .pmtiles file, or its URL if the file is hosted remotely. |
required |
Returns:
Type | Description |
---|---|
dict |
A dictionary containing the header information, including center and bounds. |
Exceptions:
Type | Description |
---|---|
ImportError |
If the pmtiles library is not installed. |
ValueError |
If the input file is not a .pmtiles file or if it does not exist. |
Examples:
>>> header = pmtiles_header("https://example.com/path/to/tiles.pmtiles")
>>> print(header["center"])
[52.5200, 13.4050]
Note
If fetching a remote PMTiles file, this function only downloads the first 127 bytes of the file to retrieve the header.
Source code in leafmap/common.py
def pmtiles_header(input_file: str):
"""
Fetch the header information from a local or remote .pmtiles file.
This function retrieves the header from a PMTiles file, either local or hosted remotely.
It deserializes the header and calculates the center and bounds of the tiles from the
given metadata in the header.
Args:
input_file (str): Path to the .pmtiles file, or its URL if the file is hosted remotely.
Returns:
dict: A dictionary containing the header information, including center and bounds.
Raises:
ImportError: If the pmtiles library is not installed.
ValueError: If the input file is not a .pmtiles file or if it does not exist.
Example:
>>> header = pmtiles_header("https://example.com/path/to/tiles.pmtiles")
>>> print(header["center"])
[52.5200, 13.4050]
Note:
If fetching a remote PMTiles file, this function only downloads the first 127 bytes
of the file to retrieve the header.
"""
import requests
from urllib.parse import urlparse
try:
from pmtiles.reader import Reader, MmapSource
from pmtiles.tile import deserialize_header
except ImportError:
print(
"pmtiles is not installed. Please install it using `pip install pmtiles`."
)
return
if not urlparse(input_file).path.endswith(".pmtiles"):
raise ValueError("Input file must be a .pmtiles file.")
if input_file.startswith("http"):
# Fetch only the first 127 bytes
headers = {"Range": "bytes=0-127"}
response = requests.get(input_file, headers=headers)
header = deserialize_header(response.content)
else:
if not os.path.exists(input_file):
raise ValueError(f"Input file {input_file} does not exist.")
with open(input_file, "rb") as f:
reader = Reader(MmapSource(f))
header = reader.header()
header["center"] = [header["center_lat_e7"] / 1e7, header["center_lon_e7"] / 1e7]
header["bounds"] = [
header["min_lon_e7"] / 1e7,
header["min_lat_e7"] / 1e7,
header["max_lon_e7"] / 1e7,
header["max_lat_e7"] / 1e7,
]
return header
pmtiles_metadata(input_file)
¶
Fetch the metadata from a local or remote .pmtiles file.
This function retrieves metadata from a PMTiles file, whether it's local or hosted remotely. If it's remote, the function fetches the header to determine the range of bytes to download for obtaining the metadata. It then reads the metadata and extracts the layer names.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
input_file |
str |
Path to the .pmtiles file, or its URL if the file is hosted remotely. |
required |
Returns:
Type | Description |
---|---|
dict |
A dictionary containing the metadata information, including layer names. |
Exceptions:
Type | Description |
---|---|
ImportError |
If the pmtiles library is not installed. |
ValueError |
If the input file is not a .pmtiles file or if it does not exist. |
Examples:
>>> metadata = pmtiles_metadata("https://example.com/path/to/tiles.pmtiles")
>>> print(metadata["layer_names"])
['buildings', 'roads']
Note
If fetching a remote PMTiles file, this function may perform multiple requests to minimize the amount of data downloaded.
Source code in leafmap/common.py
def pmtiles_metadata(input_file: str) -> Dict[str, Union[str, int, List[str]]]:
"""
Fetch the metadata from a local or remote .pmtiles file.
This function retrieves metadata from a PMTiles file, whether it's local or hosted remotely.
If it's remote, the function fetches the header to determine the range of bytes to download
for obtaining the metadata. It then reads the metadata and extracts the layer names.
Args:
input_file (str): Path to the .pmtiles file, or its URL if the file is hosted remotely.
Returns:
dict: A dictionary containing the metadata information, including layer names.
Raises:
ImportError: If the pmtiles library is not installed.
ValueError: If the input file is not a .pmtiles file or if it does not exist.
Example:
>>> metadata = pmtiles_metadata("https://example.com/path/to/tiles.pmtiles")
>>> print(metadata["layer_names"])
['buildings', 'roads']
Note:
If fetching a remote PMTiles file, this function may perform multiple requests to minimize
the amount of data downloaded.
"""
import json
import requests
from urllib.parse import urlparse
try:
from pmtiles.reader import Reader, MmapSource, MemorySource
except ImportError:
print(
"pmtiles is not installed. Please install it using `pip install pmtiles`."
)
return
# ignore uri parameters when checking file suffix
if not urlparse(input_file).path.endswith(".pmtiles"):
raise ValueError("Input file must be a .pmtiles file.")
header = pmtiles_header(input_file)
metadata_offset = header["metadata_offset"]
metadata_length = header["metadata_length"]
if input_file.startswith("http"):
headers = {"Range": f"bytes=0-{metadata_offset + metadata_length}"}
response = requests.get(input_file, headers=headers)
content = MemorySource(response.content)
metadata = Reader(content).metadata()
else:
with open(input_file, "rb") as f:
reader = Reader(MmapSource(f))
metadata = reader.metadata()
if "json" in metadata:
metadata["vector_layers"] = json.loads(metadata["json"])[
"vector_layers"
]
vector_layers = metadata["vector_layers"]
layer_names = [layer["id"] for layer in vector_layers]
if "tilestats" in metadata:
geometries = [layer["geometry"] for layer in metadata["tilestats"]["layers"]]
metadata["geometries"] = geometries
metadata["layer_names"] = layer_names
metadata["center"] = header["center"]
metadata["bounds"] = header["bounds"]
return metadata
pmtiles_style(url, layers=None, cmap='Set3', n_class=None, opacity=0.5, circle_radius=5, line_width=1, attribution='PMTiles', **kwargs)
¶
Generates a Mapbox style JSON for rendering PMTiles data.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
The URL of the PMTiles file. |
required |
layers |
str or list[str] |
The layers to include in the style. If None, all layers will be included. Defaults to None. |
None |
cmap |
str |
The color map to use for styling the layers. Defaults to "Set3". |
'Set3' |
n_class |
int |
The number of classes to use for styling. If None, the number of classes will be determined automatically based on the color map. Defaults to None. |
None |
opacity |
float |
The fill opacity for polygon layers. Defaults to 0.5. |
0.5 |
circle_radius |
int |
The circle radius for point layers. Defaults to 5. |
5 |
line_width |
int |
The line width for line layers. Defaults to 1. |
1 |
attribution |
str |
The attribution text for the data source. Defaults to "PMTiles". |
'PMTiles' |
Returns:
Type | Description |
---|---|
dict |
The Mapbox style JSON. |
Exceptions:
Type | Description |
---|---|
ValueError |
If the layers argument is not a string or a list. |
ValueError |
If a layer specified in the layers argument does not exist in the PMTiles file. |
Source code in leafmap/common.py
def pmtiles_style(
url: str,
layers: Optional[Union[str, List[str]]] = None,
cmap: str = "Set3",
n_class: Optional[int] = None,
opacity: float = 0.5,
circle_radius: int = 5,
line_width: int = 1,
attribution: str = "PMTiles",
**kwargs,
):
"""
Generates a Mapbox style JSON for rendering PMTiles data.
Args:
url (str): The URL of the PMTiles file.
layers (str or list[str], optional): The layers to include in the style. If None, all layers will be included.
Defaults to None.
cmap (str, optional): The color map to use for styling the layers. Defaults to "Set3".
n_class (int, optional): The number of classes to use for styling. If None, the number of classes will be
determined automatically based on the color map. Defaults to None.
opacity (float, optional): The fill opacity for polygon layers. Defaults to 0.5.
circle_radius (int, optional): The circle radius for point layers. Defaults to 5.
line_width (int, optional): The line width for line layers. Defaults to 1.
attribution (str, optional): The attribution text for the data source. Defaults to "PMTiles".
Returns:
dict: The Mapbox style JSON.
Raises:
ValueError: If the layers argument is not a string or a list.
ValueError: If a layer specified in the layers argument does not exist in the PMTiles file.
"""
if cmap == "Set3":
palette = [
"#8dd3c7",
"#ffffb3",
"#bebada",
"#fb8072",
"#80b1d3",
"#fdb462",
"#b3de69",
"#fccde5",
"#d9d9d9",
"#bc80bd",
"#ccebc5",
"#ffed6f",
]
elif isinstance(cmap, list):
palette = cmap
else:
from .colormaps import get_palette
palette = ["#" + c for c in get_palette(cmap, n_class)]
n_class = len(palette)
metadata = pmtiles_metadata(url)
layer_names = metadata["layer_names"]
style = {
"version": 8,
"sources": {
"source": {
"type": "vector",
"url": "pmtiles://" + url,
"attribution": attribution,
}
},
"layers": [],
}
if layers is None:
layers = layer_names
elif isinstance(layers, str):
layers = [layers]
elif isinstance(layers, list):
for layer in layers:
if layer not in layer_names:
raise ValueError(f"Layer {layer} does not exist in the PMTiles file.")
else:
raise ValueError("The layers argument must be a string or a list.")
for i, layer_name in enumerate(layers):
layer_point = {
"id": f"{layer_name}_point",
"source": "source",
"source-layer": layer_name,
"type": "circle",
"paint": {
"circle-color": palette[i % n_class],
"circle-radius": circle_radius,
},
"filter": ["==", ["geometry-type"], "Point"],
}
layer_stroke = {
"id": f"{layer_name}_stroke",
"source": "source",
"source-layer": layer_name,
"type": "line",
"paint": {
"line-color": palette[i % n_class],
"line-width": line_width,
},
"filter": ["==", ["geometry-type"], "LineString"],
}
layer_fill = {
"id": f"{layer_name}_fill",
"source": "source",
"source-layer": layer_name,
"type": "fill",
"paint": {
"fill-color": palette[i % n_class],
"fill-opacity": opacity,
},
"filter": ["==", ["geometry-type"], "Polygon"],
}
style["layers"].extend([layer_point, layer_stroke, layer_fill])
return style
png_to_gif(in_dir, out_gif, fps=10, loop=0)
¶
Convert a list of png images to gif.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_dir |
str |
The input directory containing png images. |
required |
out_gif |
str |
The output file path to the gif. |
required |
fps |
int |
Frames per second. Defaults to 10. |
10 |
loop |
bool |
controls how many times the animation repeats. 1 means that the animation will play once and then stop (displaying the last frame). A value of 0 means that the animation will repeat forever. Defaults to 0. |
0 |
Exceptions:
Type | Description |
---|---|
FileNotFoundError |
No png images could be found. |
Source code in leafmap/common.py
def png_to_gif(in_dir, out_gif, fps=10, loop=0):
"""Convert a list of png images to gif.
Args:
in_dir (str): The input directory containing png images.
out_gif (str): The output file path to the gif.
fps (int, optional): Frames per second. Defaults to 10.
loop (bool, optional): controls how many times the animation repeats. 1 means that the animation will play once and then stop (displaying the last frame). A value of 0 means that the animation will repeat forever. Defaults to 0.
Raises:
FileNotFoundError: No png images could be found.
"""
import glob
from PIL import Image
if not out_gif.endswith(".gif"):
raise ValueError("The out_gif must be a gif file.")
out_gif = os.path.abspath(out_gif)
out_dir = os.path.dirname(out_gif)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
# Create the frames
frames = []
imgs = list(glob.glob(os.path.join(in_dir, "*.png")))
imgs.sort()
if len(imgs) == 0:
raise FileNotFoundError(f"No png could be found in {in_dir}.")
for i in imgs:
new_frame = Image.open(i)
frames.append(new_frame)
# Save into a GIF file that loops forever
frames[0].save(
out_gif,
format="GIF",
append_images=frames[1:],
save_all=True,
duration=1000 / fps,
loop=loop,
)
point_to_gdf(x, y, point_crs='EPSG:4326', to_crs='EPSG:4326', **kwargs)
¶
Convert a point to a GeoDataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
x |
float |
X coordinate of the point. |
required |
y |
float |
Y coordinate of the point. |
required |
point_crs |
str |
Coordinate Reference System of the point. |
'EPSG:4326' |
Returns:
Type | Description |
---|---|
gpd.GeoDataFrame |
GeoDataFrame containing the point. |
Source code in leafmap/common.py
def point_to_gdf(x, y, point_crs="EPSG:4326", to_crs="EPSG:4326", **kwargs):
"""
Convert a point to a GeoDataFrame.
Args:
x (float): X coordinate of the point.
y (float): Y coordinate of the point.
point_crs (str): Coordinate Reference System of the point.
Returns:
gpd.GeoDataFrame: GeoDataFrame containing the point.
"""
import geopandas as gpd
from shapely.geometry import Point
# Create a Point object
point = Point(x, y)
# Convert the Point to a GeoDataFrame
gdf = gpd.GeoDataFrame([{"geometry": point}], crs=point_crs)
if to_crs != point_crs:
gdf = gdf.to_crs(to_crs)
return gdf
points_from_xy(data, x='longitude', y='latitude', z=None, crs=None, **kwargs)
¶
Create a GeoPandas GeoDataFrame from a csv or Pandas DataFrame containing x, y, z values.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
data |
str | pd.DataFrame |
A csv or Pandas DataFrame containing x, y, z values. |
required |
x |
str |
The column name for the x values. Defaults to "longitude". |
'longitude' |
y |
str |
The column name for the y values. Defaults to "latitude". |
'latitude' |
z |
str |
The column name for the z values. Defaults to None. |
None |
crs |
str | int |
The coordinate reference system for the GeoDataFrame. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
geopandas.GeoDataFrame |
A GeoPandas GeoDataFrame containing x, y, z values. |
Source code in leafmap/common.py
def points_from_xy(data, x="longitude", y="latitude", z=None, crs=None, **kwargs):
"""Create a GeoPandas GeoDataFrame from a csv or Pandas DataFrame containing x, y, z values.
Args:
data (str | pd.DataFrame): A csv or Pandas DataFrame containing x, y, z values.
x (str, optional): The column name for the x values. Defaults to "longitude".
y (str, optional): The column name for the y values. Defaults to "latitude".
z (str, optional): The column name for the z values. Defaults to None.
crs (str | int, optional): The coordinate reference system for the GeoDataFrame. Defaults to None.
Returns:
geopandas.GeoDataFrame: A GeoPandas GeoDataFrame containing x, y, z values.
"""
check_package(name="geopandas", URL="https://geopandas.org")
import geopandas as gpd
import pandas as pd
if crs is None:
crs = "epsg:4326"
if isinstance(data, pd.DataFrame):
df = data
elif isinstance(data, str):
if not data.startswith("http") and (not os.path.exists(data)):
raise FileNotFoundError("The specified input csv does not exist.")
else:
df = pd.read_csv(data, **kwargs)
else:
raise TypeError("The data must be a pandas DataFrame or a csv file path.")
gdf = gpd.GeoDataFrame(df, geometry=gpd.points_from_xy(df[x], df[y], z=z, crs=crs))
return gdf
points_to_line(data, src_lat, src_lon, dst_lat, dst_lon, crs='EPSG:4326', **kwargs)
¶
Converts source and destination coordinates into a GeoDataFrame with LineString geometries.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
data |
Union[str, pd.DataFrame, gpd.GeoDataFrame] |
Input data which can be a file path or a DataFrame. |
required |
src_lat |
str |
Column name for source latitude. |
required |
src_lon |
str |
Column name for source longitude. |
required |
dst_lat |
str |
Column name for destination latitude. |
required |
dst_lon |
str |
Column name for destination longitude. |
required |
crs |
str |
Coordinate reference system. Defaults to "EPSG:4326". |
'EPSG:4326' |
**kwargs |
Any |
Additional arguments passed to the file reading functions. |
{} |
Returns:
Type | Description |
---|---|
gpd.GeoDataFrame |
A GeoDataFrame with LineString geometries. |
Source code in leafmap/common.py
def points_to_line(
data: Union[str, pd.DataFrame],
src_lat: str,
src_lon: str,
dst_lat: str,
dst_lon: str,
crs: str = "EPSG:4326",
**kwargs: Any,
) -> "gpd.GeoDataFrame":
"""
Converts source and destination coordinates into a GeoDataFrame with LineString geometries.
Args:
data (Union[str, pd.DataFrame, gpd.GeoDataFrame]): Input data which can be a file path or a DataFrame.
src_lat (str): Column name for source latitude.
src_lon (str): Column name for source longitude.
dst_lat (str): Column name for destination latitude.
dst_lon (str): Column name for destination longitude.
crs (str, optional): Coordinate reference system. Defaults to "EPSG:4326".
**kwargs (Any): Additional arguments passed to the file reading functions.
Returns:
gpd.GeoDataFrame: A GeoDataFrame with LineString geometries.
"""
import geopandas as gpd
from shapely.geometry import LineString
if isinstance(data, str):
if data.endswith(".parquet"):
gdf = pd.read_parquet(data, **kwargs)
elif data.endswith(".csv"):
gdf = pd.read_csv(data, **kwargs)
elif data.endswith(".json"):
gdf = pd.read_json(data, **kwargs)
elif data.endswith(".xlsx"):
gdf = pd.read_excel(data, **kwargs)
else:
gdf = gpd.read_file(data, **kwargs)
elif isinstance(data, pd.DataFrame) or isinstance(data, gpd.GeoDataFrame):
gdf = data.copy()
else:
raise ValueError(
"Unsupported data type. Please provide a file path or a DataFrame."
)
# Assuming you have a GeoDataFrame 'gdf' with the source and destination coordinates
def create_polyline(row):
source_point = (row[src_lon], row[src_lat])
dst_point = (row[dst_lon], row[dst_lat])
return LineString([source_point, dst_point])
# Apply the function to create the polyline geometry
gdf["geometry"] = gdf.apply(create_polyline, axis=1)
# Set the GeoDataFrame's geometry column to the newly created geometry column
gdf = gdf.set_geometry("geometry")
gdf.crs = crs
return gdf
random_string(string_length=3)
¶
Generates a random string of fixed length.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
string_length |
int |
Fixed length. Defaults to 3. |
3 |
Returns:
Type | Description |
---|---|
str |
A random string |
Source code in leafmap/common.py
def random_string(string_length: Optional[int] = 3) -> str:
"""Generates a random string of fixed length.
Args:
string_length (int, optional): Fixed length. Defaults to 3.
Returns:
str: A random string
"""
import random
import string
# random.seed(1001)
letters = string.ascii_lowercase
return "".join(random.choice(letters) for i in range(string_length))
raster_to_vector(source, output, simplify_tolerance=None, dst_crs=None, open_args={}, **kwargs)
¶
Vectorize a raster dataset.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source |
str |
The path to the tiff file. |
required |
output |
str |
The path to the vector file. |
required |
simplify_tolerance |
float |
The maximum allowed geometry displacement. The higher this value, the smaller the number of vertices in the resulting geometry. |
None |
Source code in leafmap/common.py
def raster_to_vector(
source, output, simplify_tolerance=None, dst_crs=None, open_args={}, **kwargs
):
"""Vectorize a raster dataset.
Args:
source (str): The path to the tiff file.
output (str): The path to the vector file.
simplify_tolerance (float, optional): The maximum allowed geometry displacement.
The higher this value, the smaller the number of vertices in the resulting geometry.
"""
import rasterio
import shapely
import geopandas as gpd
from rasterio import features
with rasterio.open(source, **open_args) as src:
band = src.read()
mask = band != 0
shapes = features.shapes(band, mask=mask, transform=src.transform)
fc = [
{"geometry": shapely.geometry.shape(shape), "properties": {"value": value}}
for shape, value in shapes
]
if simplify_tolerance is not None:
for i in fc:
i["geometry"] = i["geometry"].simplify(tolerance=simplify_tolerance)
gdf = gpd.GeoDataFrame.from_features(fc)
if src.crs is not None:
gdf.set_crs(crs=src.crs, inplace=True)
if dst_crs is not None:
gdf = gdf.to_crs(dst_crs)
gdf.to_file(output, **kwargs)
read_file(data, **kwargs)
¶
Reads a file and returns a DataFrame or GeoDataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
data |
str |
The file path or a DataFrame/GeoDataFrame. |
required |
**kwargs |
Any |
Additional arguments passed to the file reading function. |
{} |
Returns:
Type | Description |
---|---|
Union[pd.DataFrame, gpd.GeoDataFrame] |
The read data as a DataFrame or GeoDataFrame. |
Exceptions:
Type | Description |
---|---|
ValueError |
If the data type is unsupported. |
Source code in leafmap/common.py
def read_file(data: str, **kwargs: Any) -> Union[pd.DataFrame, "gpd.GeoDataFrame"]:
"""
Reads a file and returns a DataFrame or GeoDataFrame.
Args:
data (str): The file path or a DataFrame/GeoDataFrame.
**kwargs (Any): Additional arguments passed to the file reading function.
Returns:
Union[pd.DataFrame, gpd.GeoDataFrame]: The read data as a DataFrame or GeoDataFrame.
Raises:
ValueError: If the data type is unsupported.
"""
import geopandas as gpd
if isinstance(data, str):
if data.endswith(".parquet"):
df = pd.read_parquet(data, **kwargs)
elif data.endswith(".csv"):
df = pd.read_csv(data, **kwargs)
elif data.endswith(".json"):
df = pd.read_json(data, **kwargs)
elif data.endswith(".xlsx"):
df = pd.read_excel(data, **kwargs)
else:
df = gpd.read_file(data, **kwargs)
elif isinstance(data, dict) or isinstance(data, list):
df = pd.DataFrame(data, **kwargs)
elif isinstance(data, pd.DataFrame) or isinstance(data, gpd.GeoDataFrame):
df = data
else:
raise ValueError(
"Unsupported data type. Please provide a file path or a DataFrame."
)
return df
read_file_from_url(url, return_type='list', encoding='utf-8')
¶
Reads a file from a URL.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
str |
The URL of the file. |
required |
return_type |
str |
The return type, can either be string or list. Defaults to "list". |
'list' |
encoding |
str |
The encoding of the file. Defaults to "utf-8". |
'utf-8' |
Exceptions:
Type | Description |
---|---|
ValueError |
The return type must be either list or string. |
Returns:
Type | Description |
---|---|
str | list |
The contents of the file. |
Source code in leafmap/common.py
def read_file_from_url(url, return_type="list", encoding="utf-8"):
"""Reads a file from a URL.
Args:
url (str): The URL of the file.
return_type (str, optional): The return type, can either be string or list. Defaults to "list".
encoding (str, optional): The encoding of the file. Defaults to "utf-8".
Raises:
ValueError: The return type must be either list or string.
Returns:
str | list: The contents of the file.
"""
from urllib.request import urlopen
if return_type == "list":
return [line.decode(encoding).rstrip() for line in urlopen(url).readlines()]
elif return_type == "string":
return urlopen(url).read().decode(encoding)
else:
raise ValueError("The return type must be either list or string.")
read_geojson(data, **kwargs)
¶
Fetches and parses a GeoJSON file from a given URL.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
data |
str |
The URL of the GeoJSON file. |
required |
**kwargs |
Any |
Additional keyword arguments to pass to the requests.get() method. |
{} |
Returns:
Type | Description |
---|---|
Dict[str, Any] |
The parsed GeoJSON data. |
Source code in leafmap/common.py
def read_geojson(data: str, **kwargs: Any) -> Dict[str, Any]:
"""
Fetches and parses a GeoJSON file from a given URL.
Args:
data (str): The URL of the GeoJSON file.
**kwargs (Any): Additional keyword arguments to pass to the requests.get() method.
Returns:
Dict[str, Any]: The parsed GeoJSON data.
"""
return requests.get(data, **kwargs).json()
read_lidar(filename, **kwargs)
¶
Read a LAS file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filename |
str |
A local file path or HTTP URL to a LAS file. |
required |
Returns:
Type | Description |
---|---|
LasData |
The LasData object return by laspy.read. |
Source code in leafmap/common.py
def read_lidar(filename, **kwargs):
"""Read a LAS file.
Args:
filename (str): A local file path or HTTP URL to a LAS file.
Returns:
LasData: The LasData object return by laspy.read.
"""
try:
import laspy
except ImportError:
print(
"The laspy package is required for this function. Use `pip install laspy[lazrs,laszip]` to install it."
)
return
if (
isinstance(filename, str)
and filename.startswith("http")
and (filename.endswith(".las") or filename.endswith(".laz"))
):
filename = github_raw_url(filename)
filename = download_file(filename)
return laspy.read(filename, **kwargs)
read_netcdf(filename, **kwargs)
¶
Read a netcdf file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filename |
str |
File path or HTTP URL to the netcdf file. |
required |
Exceptions:
Type | Description |
---|---|
ImportError |
If the xarray or rioxarray package is not installed. |
FileNotFoundError |
If the netcdf file is not found. |
Returns:
Type | Description |
---|---|
xarray.Dataset |
The netcdf file as an xarray dataset. |
Source code in leafmap/common.py
def read_netcdf(filename, **kwargs):
"""Read a netcdf file.
Args:
filename (str): File path or HTTP URL to the netcdf file.
Raises:
ImportError: If the xarray or rioxarray package is not installed.
FileNotFoundError: If the netcdf file is not found.
Returns:
xarray.Dataset: The netcdf file as an xarray dataset.
"""
try:
import xarray as xr
except ImportError as e:
raise ImportError(e)
if filename.startswith("http"):
filename = download_file(filename)
if not os.path.exists(filename):
raise FileNotFoundError(f"{filename} does not exist.")
xds = xr.open_dataset(filename, **kwargs)
return xds
read_parquet(source, geometry=None, columns=None, exclude=None, db=None, table_name=None, sql=None, limit=None, src_crs=None, dst_crs=None, return_type='gdf', **kwargs)
¶
Read Parquet data from a source and return a GeoDataFrame or DataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source |
str |
The path to the Parquet file or directory containing Parquet files. |
required |
geometry |
str |
The name of the geometry column. Defaults to None. |
None |
columns |
str or list |
The columns to select. Defaults to None (select all columns). |
None |
exclude |
str or list |
The columns to exclude from the selection. Defaults to None. |
None |
db |
str |
The DuckDB database path or alias. Defaults to None. |
None |
table_name |
str |
The name of the table in the DuckDB database. Defaults to None. |
None |
sql |
str |
The SQL query to execute. Defaults to None. |
None |
limit |
int |
The maximum number of rows to return. Defaults to None (return all rows). |
None |
src_crs |
str |
The source CRS (Coordinate Reference System) of the geometries. Defaults to None. |
None |
dst_crs |
str |
The target CRS to reproject the geometries. Defaults to None. |
None |
return_type |
str |
The type of object to return: - 'gdf': GeoDataFrame (default) - 'df': DataFrame - 'numpy': NumPy array - 'arrow': Arrow Table - 'polars': Polars DataFrame |
'gdf' |
**kwargs |
Additional keyword arguments that are passed to the DuckDB connection. |
{} |
Returns:
Type | Description |
---|---|
Union[gpd.GeoDataFrame, pd.DataFrame, np.ndarray] |
The loaded data. |
Exceptions:
Type | Description |
---|---|
ValueError |
If the columns or exclude arguments are not of the correct type. |
Source code in leafmap/common.py
def read_parquet(
source: str,
geometry: Optional[str] = None,
columns: Optional[Union[str, list]] = None,
exclude: Optional[Union[str, list]] = None,
db: Optional[str] = None,
table_name: Optional[str] = None,
sql: Optional[str] = None,
limit: Optional[int] = None,
src_crs: Optional[str] = None,
dst_crs: Optional[str] = None,
return_type: str = "gdf",
**kwargs,
):
"""
Read Parquet data from a source and return a GeoDataFrame or DataFrame.
Args:
source (str): The path to the Parquet file or directory containing Parquet files.
geometry (str, optional): The name of the geometry column. Defaults to None.
columns (str or list, optional): The columns to select. Defaults to None (select all columns).
exclude (str or list, optional): The columns to exclude from the selection. Defaults to None.
db (str, optional): The DuckDB database path or alias. Defaults to None.
table_name (str, optional): The name of the table in the DuckDB database. Defaults to None.
sql (str, optional): The SQL query to execute. Defaults to None.
limit (int, optional): The maximum number of rows to return. Defaults to None (return all rows).
src_crs (str, optional): The source CRS (Coordinate Reference System) of the geometries. Defaults to None.
dst_crs (str, optional): The target CRS to reproject the geometries. Defaults to None.
return_type (str, optional): The type of object to return:
- 'gdf': GeoDataFrame (default)
- 'df': DataFrame
- 'numpy': NumPy array
- 'arrow': Arrow Table
- 'polars': Polars DataFrame
**kwargs: Additional keyword arguments that are passed to the DuckDB connection.
Returns:
Union[gpd.GeoDataFrame, pd.DataFrame, np.ndarray]: The loaded data.
Raises:
ValueError: If the columns or exclude arguments are not of the correct type.
"""
import duckdb
if isinstance(db, str):
con = duckdb.connect(db)
else:
con = duckdb.connect()
con.install_extension("httpfs")
con.load_extension("httpfs")
con.install_extension("spatial")
con.load_extension("spatial")
if columns is None:
columns = "*"
elif isinstance(columns, list):
columns = ", ".join(columns)
elif not isinstance(columns, str):
raise ValueError("columns must be a list or a string.")
if exclude is not None:
if isinstance(exclude, list):
exclude = ", ".join(exclude)
elif not isinstance(exclude, str):
raise ValueError("exclude_columns must be a list or a string.")
columns = f"{columns} EXCLUDE {exclude}"
if return_type in ["df", "numpy", "arrow", "polars"]:
if sql is None:
sql = f"SELECT {columns} FROM '{source}'"
if limit is not None:
sql += f" LIMIT {limit}"
if return_type == "df":
result = con.sql(sql, **kwargs).df()
elif return_type == "numpy":
result = con.sql(sql, **kwargs).fetchnumpy()
elif return_type == "arrow":
result = con.sql(sql, **kwargs).arrow()
elif return_type == "polars":
result = con.sql(sql, **kwargs).pl()
if table_name is not None:
con.sql(f"CREATE OR REPLACE TABLE {table_name} AS FROM result", **kwargs)
elif return_type == "gdf":
if geometry is None:
geometry = "geometry"
if sql is None:
# if src_crs is not None and dst_crs is not None:
# geom_sql = f"ST_AsText(ST_Transform(ST_GeomFromWKB({geometry}), '{src_crs}', '{dst_crs}', true)) AS {geometry}"
# else:
geom_sql = f"ST_AsText(ST_GeomFromWKB({geometry})) AS {geometry}"
sql = f"SELECT {columns} EXCLUDE {geometry}, {geom_sql} FROM '{source}'"
if limit is not None:
sql += f" LIMIT {limit}"
df = con.sql(sql, **kwargs).df()
if table_name is not None:
con.sql(f"CREATE OR REPLACE TABLE {table_name} AS FROM df", **kwargs)
result = df_to_gdf(df, geometry=geometry, src_crs=src_crs, dst_crs=dst_crs)
con.close()
return result
read_postgis(sql, con, geom_col='geom', crs=None, **kwargs)
¶
Reads data from a PostGIS database and returns a GeoDataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
sql |
str |
SQL query to execute in selecting entries from database, or name of the table to read from the database. |
required |
con |
sqlalchemy.engine.Engine |
Active connection to the database to query. |
required |
geom_col |
str |
Column name to convert to shapely geometries. Defaults to "geom". |
'geom' |
crs |
str | dict |
CRS to use for the returned GeoDataFrame; if not set, tries to determine CRS from the SRID associated with the first geometry in the database, and assigns that to all geometries. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
[type] |
[description] |
Source code in leafmap/common.py
def read_postgis(sql, con, geom_col="geom", crs=None, **kwargs):
"""Reads data from a PostGIS database and returns a GeoDataFrame.
Args:
sql (str): SQL query to execute in selecting entries from database, or name of the table to read from the database.
con (sqlalchemy.engine.Engine): Active connection to the database to query.
geom_col (str, optional): Column name to convert to shapely geometries. Defaults to "geom".
crs (str | dict, optional): CRS to use for the returned GeoDataFrame; if not set, tries to determine CRS from the SRID associated with the first geometry in the database, and assigns that to all geometries. Defaults to None.
Returns:
[type]: [description]
"""
check_package(name="geopandas", URL="https://geopandas.org")
import geopandas as gpd
gdf = gpd.read_postgis(sql, con, geom_col, crs, **kwargs)
return gdf
read_raster(source, window=None, return_array=True, coord_crs=None, request_payer='bucket-owner', env_args={}, open_args={}, **kwargs)
¶
Read a raster from S3.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source |
str |
The path to the raster on S3. |
required |
window |
tuple |
The window (col_off, row_off, width, height) to read. Defaults to None. |
None |
return_array |
bool |
Whether to return a numpy array. Defaults to True. |
True |
coord_crs |
str |
The coordinate CRS of the input coordinates. Defaults to None. |
None |
request_payer |
str |
Specifies who pays for the download from S3. Can be "bucket-owner" or "requester". Defaults to "bucket-owner". |
'bucket-owner' |
env_args |
dict |
Additional arguments to pass to rasterio.Env(). Defaults to {}. |
{} |
open_args |
dict |
Additional arguments to pass to rasterio.open(). Defaults to {}. |
{} |
Returns:
Type | Description |
---|---|
np.ndarray |
The raster as a numpy array. |
Source code in leafmap/common.py
def read_raster(
source,
window=None,
return_array=True,
coord_crs=None,
request_payer="bucket-owner",
env_args={},
open_args={},
**kwargs,
):
"""Read a raster from S3.
Args:
source (str): The path to the raster on S3.
window (tuple, optional): The window (col_off, row_off, width, height) to read. Defaults to None.
return_array (bool, optional): Whether to return a numpy array. Defaults to True.
coord_crs (str, optional): The coordinate CRS of the input coordinates. Defaults to None.
request_payer (str, optional): Specifies who pays for the download from S3.
Can be "bucket-owner" or "requester". Defaults to "bucket-owner".
env_args (dict, optional): Additional arguments to pass to rasterio.Env(). Defaults to {}.
open_args (dict, optional): Additional arguments to pass to rasterio.open(). Defaults to {}.
Returns:
np.ndarray: The raster as a numpy array.
"""
import rasterio
from rasterio.windows import Window
with rasterio.Env(AWS_REQUEST_PAYER=request_payer, **env_args):
src = rasterio.open(source, **open_args)
if not return_array:
return src
else:
if window is None:
window = Window(0, 0, src.width, src.height)
else:
if isinstance(window, list):
coords = coords_to_xy(
source,
window,
coord_crs,
env_args=env_args,
open_args=open_args,
)
window = xy_to_window(coords)
window = Window(*window)
array = src.read(window=window, **kwargs)
return array
read_rasters(sources, window=None, coord_crs=None, request_payer='bucket-owner', env_args={}, open_args={}, **kwargs)
¶
Read a raster from S3.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
sources |
str |
The list of paths to the raster files. |
required |
window |
tuple |
The window (col_off, row_off, width, height) to read. Defaults to None. |
None |
coord_crs |
str |
The coordinate CRS of the input coordinates. Defaults to None. |
None |
request_payer |
str |
Specifies who pays for the download from S3. Can be "bucket-owner" or "requester". Defaults to "bucket-owner". |
'bucket-owner' |
env_args |
dict |
Additional arguments to pass to rasterio.Env(). Defaults to {}. |
{} |
open_args |
dict |
Additional arguments to pass to rasterio.open(). Defaults to {}. |
{} |
Returns:
Type | Description |
---|---|
np.ndarray |
The raster as a numpy array. |
Source code in leafmap/common.py
def read_rasters(
sources,
window=None,
coord_crs=None,
request_payer="bucket-owner",
env_args={},
open_args={},
**kwargs,
):
"""Read a raster from S3.
Args:
sources (str): The list of paths to the raster files.
window (tuple, optional): The window (col_off, row_off, width, height) to read. Defaults to None.
coord_crs (str, optional): The coordinate CRS of the input coordinates. Defaults to None.
request_payer (str, optional): Specifies who pays for the download from S3.
Can be "bucket-owner" or "requester". Defaults to "bucket-owner".
env_args (dict, optional): Additional arguments to pass to rasterio.Env(). Defaults to {}.
open_args (dict, optional): Additional arguments to pass to rasterio.open(). Defaults to {}.
Returns:
np.ndarray: The raster as a numpy array.
"""
import numpy as np
if not isinstance(sources, list):
sources = [sources]
array_list = []
for source in sources:
array = read_raster(
source,
window,
True,
coord_crs,
request_payer,
env_args,
open_args,
**kwargs,
)
array_list.append(array)
result = np.concatenate(array_list, axis=0)
return result
reduce_gif_size(in_gif, out_gif=None)
¶
Reduces a GIF image using ffmpeg.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_gif |
str |
The input file path to the GIF image. |
required |
out_gif |
str |
The output file path to the GIF image. Defaults to None. |
None |
Source code in leafmap/common.py
def reduce_gif_size(in_gif, out_gif=None):
"""Reduces a GIF image using ffmpeg.
Args:
in_gif (str): The input file path to the GIF image.
out_gif (str, optional): The output file path to the GIF image. Defaults to None.
"""
try:
import ffmpeg
except ImportError:
print("ffmpeg is not installed on your computer. Skip reducing gif size.")
return
warnings.filterwarnings("ignore")
if not is_tool("ffmpeg"):
print("ffmpeg is not installed on your computer. Skip reducing gif size.")
return
if not os.path.exists(in_gif):
print("The input gif file does not exist.")
return
if out_gif is None:
out_gif = in_gif
elif not os.path.exists(os.path.dirname(out_gif)):
os.makedirs(os.path.dirname(out_gif))
if in_gif == out_gif:
tmp_gif = in_gif.replace(".gif", "_tmp.gif")
shutil.copyfile(in_gif, tmp_gif)
stream = ffmpeg.input(tmp_gif)
stream = ffmpeg.output(stream, in_gif, loglevel="quiet").overwrite_output()
ffmpeg.run(stream)
os.remove(tmp_gif)
else:
stream = ffmpeg.input(in_gif)
stream = ffmpeg.output(stream, out_gif, loglevel="quiet").overwrite_output()
ffmpeg.run(stream)
regularize(source, output=None, crs='EPSG:4326', **kwargs)
¶
Regularize a polygon GeoDataFrame.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source |
str | gpd.GeoDataFrame |
The input file path or a GeoDataFrame. |
required |
output |
str |
The output file path. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
gpd.GeoDataFrame |
The output GeoDataFrame. |
Source code in leafmap/common.py
def regularize(source, output=None, crs="EPSG:4326", **kwargs):
"""Regularize a polygon GeoDataFrame.
Args:
source (str | gpd.GeoDataFrame): The input file path or a GeoDataFrame.
output (str, optional): The output file path. Defaults to None.
Returns:
gpd.GeoDataFrame: The output GeoDataFrame.
"""
import geopandas as gpd
if isinstance(source, str):
gdf = gpd.read_file(source)
elif isinstance(source, gpd.GeoDataFrame):
gdf = source
else:
raise ValueError("The input source must be a GeoDataFrame or a file path.")
polygons = gdf.geometry.apply(lambda geom: geom.minimum_rotated_rectangle)
result = gpd.GeoDataFrame(geometry=polygons, data=gdf.drop("geometry", axis=1))
if crs is not None:
result.to_crs(crs, inplace=True)
if output is not None:
result.to_file(output, **kwargs)
else:
return result
remove_port_from_string(data)
¶
Removes the port number from all URLs in the given string.
Args:: data (str): The input string containing URLs.
Returns:
Type | Description |
---|---|
str |
The string with port numbers removed from all URLs. |
Source code in leafmap/common.py
def remove_port_from_string(data: str) -> str:
"""
Removes the port number from all URLs in the given string.
Args::
data (str): The input string containing URLs.
Returns:
str: The string with port numbers removed from all URLs.
"""
import re
# Regular expression to match URLs with port numbers
url_with_port_pattern = re.compile(r"(http://[\d\w.]+):\d+")
# Function to remove the port from the matched URLs
def remove_port(match):
return match.group(1)
# Substitute the URLs with ports removed
result = url_with_port_pattern.sub(remove_port, data)
return result
replace_hyphens_in_keys(d)
¶
Recursively replaces hyphens with underscores in dictionary keys.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
d |
Union[Dict, List, Any] |
The input dictionary, list or any other data type. |
required |
Returns:
Type | Description |
---|---|
Union[Dict, List, Any] |
The modified dictionary or list with keys having hyphens replaced with underscores, or the original input if it's not a dictionary or list. |
Source code in leafmap/common.py
def replace_hyphens_in_keys(d: Union[Dict, List, Any]) -> Union[Dict, List, Any]:
"""
Recursively replaces hyphens with underscores in dictionary keys.
Args:
d (Union[Dict, List, Any]): The input dictionary, list or any other data type.
Returns:
Union[Dict, List, Any]: The modified dictionary or list with keys having hyphens replaced with underscores,
or the original input if it's not a dictionary or list.
"""
if isinstance(d, dict):
return {k.replace("-", "_"): replace_hyphens_in_keys(v) for k, v in d.items()}
elif isinstance(d, list):
return [replace_hyphens_in_keys(i) for i in d]
else:
return d
replace_top_level_hyphens(d)
¶
Replaces hyphens with underscores in top-level dictionary keys.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
d |
Union[Dict, Any] |
The input dictionary or any other data type. |
required |
Returns:
Type | Description |
---|---|
Union[Dict, Any] |
The modified dictionary with top-level keys having hyphens replaced with underscores, or the original input if it's not a dictionary. |
Source code in leafmap/common.py
def replace_top_level_hyphens(d: Union[Dict, Any]) -> Union[Dict, Any]:
"""
Replaces hyphens with underscores in top-level dictionary keys.
Args:
d (Union[Dict, Any]): The input dictionary or any other data type.
Returns:
Union[Dict, Any]: The modified dictionary with top-level keys having hyphens replaced with underscores,
or the original input if it's not a dictionary.
"""
if isinstance(d, dict):
return {k.replace("-", "_"): v for k, v in d.items()}
return d
reproject(image, output, dst_crs='EPSG:4326', resampling='nearest', to_cog=True, **kwargs)
¶
Reprojects an image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image |
str |
The input image filepath. |
required |
output |
str |
The output image filepath. |
required |
dst_crs |
str |
The destination CRS. Defaults to "EPSG:4326". |
'EPSG:4326' |
resampling |
Resampling |
The resampling method. Defaults to "nearest". |
'nearest' |
to_cog |
bool |
Whether to convert the output image to a Cloud Optimized GeoTIFF. Defaults to True. |
True |
**kwargs |
Additional keyword arguments to pass to rasterio.open. |
{} |
Source code in leafmap/common.py
def reproject(
image, output, dst_crs="EPSG:4326", resampling="nearest", to_cog=True, **kwargs
):
"""Reprojects an image.
Args:
image (str): The input image filepath.
output (str): The output image filepath.
dst_crs (str, optional): The destination CRS. Defaults to "EPSG:4326".
resampling (Resampling, optional): The resampling method. Defaults to "nearest".
to_cog (bool, optional): Whether to convert the output image to a Cloud Optimized GeoTIFF. Defaults to True.
**kwargs: Additional keyword arguments to pass to rasterio.open.
"""
import rasterio as rio
from rasterio.warp import calculate_default_transform, reproject, Resampling
if isinstance(resampling, str):
resampling = getattr(Resampling, resampling)
image = os.path.abspath(image)
output = os.path.abspath(output)
if not os.path.exists(os.path.dirname(output)):
os.makedirs(os.path.dirname(output))
with rio.open(image, **kwargs) as src:
transform, width, height = calculate_default_transform(
src.crs, dst_crs, src.width, src.height, *src.bounds
)
kwargs = src.meta.copy()
kwargs.update(
{
"crs": dst_crs,
"transform": transform,
"width": width,
"height": height,
}
)
with rio.open(output, "w", **kwargs) as dst:
for i in range(1, src.count + 1):
reproject(
source=rio.band(src, i),
destination=rio.band(dst, i),
src_transform=src.transform,
src_crs=src.crs,
dst_transform=transform,
dst_crs=dst_crs,
resampling=resampling,
**kwargs,
)
if to_cog:
image_to_cog(output, output)
rgb_to_hex(rgb=(255, 255, 255))
¶
Converts RGB to hex color. In RGB color R stands for Red, G stands for Green, and B stands for Blue, and it ranges from the decimal value of 0 – 255.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
rgb |
tuple |
RGB color code as a tuple of (red, green, blue). Defaults to (255, 255, 255). |
(255, 255, 255) |
Returns:
Type | Description |
---|---|
str |
hex color code |
Source code in leafmap/common.py
def rgb_to_hex(rgb: Optional[Tuple[int, int, int]] = (255, 255, 255)) -> str:
"""Converts RGB to hex color. In RGB color R stands for Red, G stands for Green, and B stands for Blue, and it ranges from the decimal value of 0 – 255.
Args:
rgb (tuple, optional): RGB color code as a tuple of (red, green, blue). Defaults to (255, 255, 255).
Returns:
str: hex color code
"""
return "%02x%02x%02x" % rgb
s3_download_file(filename=None, bucket=None, key=None, outfile=None, **kwargs)
¶
Download a file from S3.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filename |
str |
The full path to the file. Defaults to None. |
None |
bucket |
str |
The name of the bucket. Defaults to None. |
None |
key |
str |
The key of the file. Defaults to None. |
None |
outfile |
str |
The name of the output file. Defaults to None. |
None |
Exceptions:
Type | Description |
---|---|
ImportError |
If boto3 is not installed. |
Source code in leafmap/common.py
def s3_download_file(filename=None, bucket=None, key=None, outfile=None, **kwargs):
"""Download a file from S3.
Args:
filename (str, optional): The full path to the file. Defaults to None.
bucket (str, optional): The name of the bucket. Defaults to None.
key (str, optional): The key of the file. Defaults to None.
outfile (str, optional): The name of the output file. Defaults to None.
Raises:
ImportError: If boto3 is not installed.
"""
if os.environ.get("USE_MKDOCS") is not None:
return
try:
import boto3
except ImportError:
raise ImportError("boto3 is not installed. Install it with pip install boto3")
client = boto3.client("s3", **kwargs)
if filename is not None:
bucket = filename.split("/")[2]
key = "/".join(filename.split("/")[3:])
if outfile is None:
outfile = key.split("/")[-1]
if not os.path.exists(outfile):
client.download_file(bucket, key, outfile)
else:
print(f"File already exists: {outfile}")
s3_download_files(filenames=None, bucket=None, keys=None, outdir=None, quiet=False, **kwargs)
¶
Download multiple files from S3.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filenames |
list |
A list of filenames. Defaults to None. |
None |
bucket |
str |
The name of the bucket. Defaults to None. |
None |
keys |
list |
A list of keys. Defaults to None. |
None |
outdir |
str |
The name of the output directory. Defaults to None. |
None |
quiet |
bool |
Suppress output. Defaults to False. |
False |
Exceptions:
Type | Description |
---|---|
ValueError |
If neither filenames or keys are provided. |
Source code in leafmap/common.py
def s3_download_files(
filenames=None, bucket=None, keys=None, outdir=None, quiet=False, **kwargs
):
"""Download multiple files from S3.
Args:
filenames (list, optional): A list of filenames. Defaults to None.
bucket (str, optional): The name of the bucket. Defaults to None.
keys (list, optional): A list of keys. Defaults to None.
outdir (str, optional): The name of the output directory. Defaults to None.
quiet (bool, optional): Suppress output. Defaults to False.
Raises:
ValueError: If neither filenames or keys are provided.
"""
if keys is None:
keys = []
if filenames is not None:
if isinstance(filenames, list):
for filename in filenames:
bucket = filename.split("/")[2]
key = "/".join(filename.split("/")[3:])
keys.append(key)
elif filenames is None and keys is None:
raise ValueError("Either filenames or keys must be provided")
for index, key in enumerate(keys):
if outdir is not None:
if not os.path.exists(outdir):
os.makedirs(outdir)
outfile = os.path.join(outdir, key.split("/")[-1])
else:
outfile = key.split("/")[-1]
if not quiet:
print(f"Downloading {index+1} of {len(keys)}: {outfile}")
s3_download_file(bucket=bucket, key=key, outfile=outfile, **kwargs)
s3_get_object(bucket, key, output=None, chunk_size=1048576, request_payer='bucket-owner', quiet=False, client_args={}, **kwargs)
¶
Download a file from S3.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
bucket |
str |
The name of the bucket. |
required |
key |
key |
The key of the file. |
required |
output |
str |
The name of the output file. Defaults to None. |
None |
chunk_size |
int |
The chunk size in bytes. Defaults to 1024 * 1024. |
1048576 |
request_payer |
str |
Specifies who pays for the download from S3. |
'bucket-owner' |
quiet |
bool |
Suppress output. Defaults to False. Can be "bucket-owner" or "requester". Defaults to "bucket-owner". |
False |
client_args |
dict |
Additional arguments to pass to boto3.client(). Defaults to {}. |
{} |
**kwargs |
Additional arguments to pass to boto3.client().get_object(). |
{} |
Source code in leafmap/common.py
def s3_get_object(
bucket,
key,
output=None,
chunk_size=1024 * 1024,
request_payer="bucket-owner",
quiet=False,
client_args={},
**kwargs,
):
"""Download a file from S3.
Args:
bucket (str): The name of the bucket.
key (key): The key of the file.
output (str, optional): The name of the output file. Defaults to None.
chunk_size (int, optional): The chunk size in bytes. Defaults to 1024 * 1024.
request_payer (str, optional): Specifies who pays for the download from S3.
quiet (bool, optional): Suppress output. Defaults to False.
Can be "bucket-owner" or "requester". Defaults to "bucket-owner".
client_args (dict, optional): Additional arguments to pass to boto3.client(). Defaults to {}.
**kwargs: Additional arguments to pass to boto3.client().get_object().
"""
try:
import boto3
except ImportError:
raise ImportError("boto3 is not installed. Install it with pip install boto3")
# Set up the S3 client
s3 = boto3.client("s3", **client_args)
if output is None:
output = key.split("/")[-1]
out_dir = os.path.dirname(os.path.abspath(output))
if not os.path.exists(out_dir):
os.makedirs(out_dir)
# Set up the progress bar
def progress_callback(bytes_amount):
# This function will be called by the StreamingBody object
# to report the number of bytes downloaded so far
total_size = int(response["ContentLength"])
progress_percent = int(bytes_amount / total_size * 100)
if not quiet:
print(f"\rDownloading: {progress_percent}% complete.", end="")
# Download the file
response = s3.get_object(
Bucket=bucket, Key=key, RequestPayer=request_payer, **kwargs
)
# Save the file to disk
with open(output, "wb") as f:
# Use the StreamingBody object to read the file in chunks
# and track the download progress
body = response["Body"]
downloaded_bytes = 0
for chunk in body.iter_chunks(chunk_size=chunk_size):
f.write(chunk)
downloaded_bytes += len(chunk)
progress_callback(downloaded_bytes)
s3_get_objects(bucket, keys=None, out_dir=None, prefix=None, limit=None, ext=None, chunk_size=1048576, request_payer='bucket-owner', quiet=True, client_args={}, **kwargs)
¶
Download multiple files from S3.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
bucket |
str |
The name of the bucket. |
required |
keys |
list |
A list of keys. Defaults to None. |
None |
out_dir |
str |
The name of the output directory. Defaults to None. |
None |
prefix |
str |
Limits the response to keys that begin with the specified prefix. Defaults to None. |
None |
limit |
int |
The maximum number of keys returned in the response body. |
None |
ext |
str |
Filter by file extension. Defaults to None. |
None |
chunk_size |
int |
The chunk size in bytes. Defaults to 1024 * 1024. |
1048576 |
request_payer |
str |
Specifies who pays for the download from S3. Can be "bucket-owner" or "requester". Defaults to "bucket-owner". |
'bucket-owner' |
quiet |
bool |
Suppress output. Defaults to True. |
True |
client_args |
dict |
Additional arguments to pass to boto3.client(). Defaults to {}. |
{} |
**kwargs |
Additional arguments to pass to boto3.client().get_object(). |
{} |
Source code in leafmap/common.py
def s3_get_objects(
bucket,
keys=None,
out_dir=None,
prefix=None,
limit=None,
ext=None,
chunk_size=1024 * 1024,
request_payer="bucket-owner",
quiet=True,
client_args={},
**kwargs,
):
"""Download multiple files from S3.
Args:
bucket (str): The name of the bucket.
keys (list, optional): A list of keys. Defaults to None.
out_dir (str, optional): The name of the output directory. Defaults to None.
prefix (str, optional): Limits the response to keys that begin with the specified prefix. Defaults to None.
limit (int, optional): The maximum number of keys returned in the response body.
ext (str, optional): Filter by file extension. Defaults to None.
chunk_size (int, optional): The chunk size in bytes. Defaults to 1024 * 1024.
request_payer (str, optional): Specifies who pays for the download from S3.
Can be "bucket-owner" or "requester". Defaults to "bucket-owner".
quiet (bool, optional): Suppress output. Defaults to True.
client_args (dict, optional): Additional arguments to pass to boto3.client(). Defaults to {}.
**kwargs: Additional arguments to pass to boto3.client().get_object().
"""
try:
import boto3
except ImportError:
raise ImportError("boto3 is not installed. Install it with pip install boto3")
if out_dir is None:
out_dir = os.getcwd()
if keys is None:
fullpath = False
keys = s3_list_objects(
bucket, prefix, limit, ext, fullpath, request_payer, client_args, **kwargs
)
for index, key in enumerate(keys):
print(f"Downloading {index+1} of {len(keys)}: {key}")
output = os.path.join(out_dir, key.split("/")[-1])
s3_get_object(
bucket, key, output, chunk_size, request_payer, quiet, client_args, **kwargs
)
s3_list_objects(bucket, prefix=None, limit=None, ext=None, fullpath=True, request_payer='bucket-owner', client_args={}, **kwargs)
¶
List objects in a S3 bucket
Parameters:
Name | Type | Description | Default |
---|---|---|---|
bucket |
str |
The name of the bucket. |
required |
prefix |
str |
Limits the response to keys that begin with the specified prefix. Defaults to None. |
None |
limit |
init |
The maximum number of keys returned in the response body. |
None |
ext |
str |
Filter by file extension. Defaults to None. |
None |
fullpath |
bool |
Return full path. Defaults to True. |
True |
request_payer |
str |
Specifies who pays for the download from S3. Can be "bucket-owner" or "requester". Defaults to "bucket-owner". |
'bucket-owner' |
client_args |
dict |
Additional arguments to pass to boto3.client(). Defaults to {}. |
{} |
Returns:
Type | Description |
---|---|
list |
List of objects. |
Source code in leafmap/common.py
def s3_list_objects(
bucket,
prefix=None,
limit=None,
ext=None,
fullpath=True,
request_payer="bucket-owner",
client_args={},
**kwargs,
):
"""List objects in a S3 bucket
Args:
bucket (str): The name of the bucket.
prefix (str, optional): Limits the response to keys that begin with the specified prefix. Defaults to None.
limit (init, optional): The maximum number of keys returned in the response body.
ext (str, optional): Filter by file extension. Defaults to None.
fullpath (bool, optional): Return full path. Defaults to True.
request_payer (str, optional): Specifies who pays for the download from S3.
Can be "bucket-owner" or "requester". Defaults to "bucket-owner".
client_args (dict, optional): Additional arguments to pass to boto3.client(). Defaults to {}.
Returns:
list: List of objects.
"""
try:
import boto3
except ImportError:
raise ImportError("boto3 is not installed. Install it with pip install boto3")
client = boto3.client("s3", **client_args)
if prefix is not None:
kwargs["Prefix"] = prefix
files = []
kwargs["RequestPayer"] = request_payer
if isinstance(limit, int) and limit < 1000:
kwargs["MaxKeys"] = limit
response = client.list_objects_v2(Bucket=bucket, **kwargs)
for obj in response["Contents"]:
files.append(obj)
else:
paginator = client.get_paginator("list_objects_v2")
pages = paginator.paginate(Bucket=bucket, **kwargs)
for page in pages:
files.extend(page.get("Contents", []))
if ext is not None:
files = [f for f in files if f["Key"].endswith(ext)]
if fullpath:
return [f"s3://{bucket}/{r['Key']}" for r in files]
else:
return [r["Key"] for r in files]
save_colorbar(out_fig=None, width=4.0, height=0.3, vmin=0, vmax=1.0, palette=None, vis_params=None, cmap='gray', discrete=False, label=None, label_size=10, label_weight='normal', tick_size=8, bg_color='white', orientation='horizontal', dpi='figure', transparent=False, show_colorbar=True, **kwargs)
¶
Create a standalone colorbar and save it as an image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
out_fig |
str |
Path to the output image. |
None |
width |
float |
Width of the colorbar in inches. Default is 4.0. |
4.0 |
height |
float |
Height of the colorbar in inches. Default is 0.3. |
0.3 |
vmin |
float |
Minimum value of the colorbar. Default is 0. |
0 |
vmax |
float |
Maximum value of the colorbar. Default is 1.0. |
1.0 |
palette |
list |
List of colors to use for the colorbar. It can also be a cmap name, such as ndvi, ndwi, dem, coolwarm. Default is None. |
None |
vis_params |
dict |
Visualization parameters as a dictionary. See https://developers.google.com/earth-engine/guides/image_visualization for options. |
None |
cmap |
str |
Matplotlib colormap. Defaults to "gray". See https://matplotlib.org/3.3.4/tutorials/colors/colormaps.html#sphx-glr-tutorials-colors-colormaps-py for options. |
'gray' |
discrete |
bool |
Whether to create a discrete colorbar. Defaults to False. |
False |
label |
str |
Label for the colorbar. Defaults to None. |
None |
label_size |
int |
Font size for the colorbar label. Defaults to 12. |
10 |
label_weight |
str |
Font weight for the colorbar label, can be "normal", "bold", etc. Defaults to "normal". |
'normal' |
tick_size |
int |
Font size for the colorbar tick labels. Defaults to 10. |
8 |
bg_color |
str |
Background color for the colorbar. Defaults to "white". |
'white' |
orientation |
str |
Orientation of the colorbar, such as "vertical" and "horizontal". Defaults to "horizontal". |
'horizontal' |
dpi |
float | str |
The resolution in dots per inch. If 'figure', use the figure's dpi value. Defaults to "figure". |
'figure' |
transparent |
bool |
Whether to make the background transparent. Defaults to False. |
False |
show_colorbar |
bool |
Whether to show the colorbar. Defaults to True. |
True |
**kwargs |
Other keyword arguments to pass to matplotlib.pyplot.savefig(). |
{} |
Returns:
Type | Description |
---|---|
str |
Path to the output image. |
Source code in leafmap/common.py
def save_colorbar(
out_fig=None,
width=4.0,
height=0.3,
vmin=0,
vmax=1.0,
palette=None,
vis_params=None,
cmap="gray",
discrete=False,
label=None,
label_size=10,
label_weight="normal",
tick_size=8,
bg_color="white",
orientation="horizontal",
dpi="figure",
transparent=False,
show_colorbar=True,
**kwargs,
):
"""Create a standalone colorbar and save it as an image.
Args:
out_fig (str): Path to the output image.
width (float): Width of the colorbar in inches. Default is 4.0.
height (float): Height of the colorbar in inches. Default is 0.3.
vmin (float): Minimum value of the colorbar. Default is 0.
vmax (float): Maximum value of the colorbar. Default is 1.0.
palette (list): List of colors to use for the colorbar. It can also be a cmap name, such as ndvi, ndwi, dem, coolwarm. Default is None.
vis_params (dict): Visualization parameters as a dictionary. See https://developers.google.com/earth-engine/guides/image_visualization for options.
cmap (str, optional): Matplotlib colormap. Defaults to "gray". See https://matplotlib.org/3.3.4/tutorials/colors/colormaps.html#sphx-glr-tutorials-colors-colormaps-py for options.
discrete (bool, optional): Whether to create a discrete colorbar. Defaults to False.
label (str, optional): Label for the colorbar. Defaults to None.
label_size (int, optional): Font size for the colorbar label. Defaults to 12.
label_weight (str, optional): Font weight for the colorbar label, can be "normal", "bold", etc. Defaults to "normal".
tick_size (int, optional): Font size for the colorbar tick labels. Defaults to 10.
bg_color (str, optional): Background color for the colorbar. Defaults to "white".
orientation (str, optional): Orientation of the colorbar, such as "vertical" and "horizontal". Defaults to "horizontal".
dpi (float | str, optional): The resolution in dots per inch. If 'figure', use the figure's dpi value. Defaults to "figure".
transparent (bool, optional): Whether to make the background transparent. Defaults to False.
show_colorbar (bool, optional): Whether to show the colorbar. Defaults to True.
**kwargs: Other keyword arguments to pass to matplotlib.pyplot.savefig().
Returns:
str: Path to the output image.
"""
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
from .colormaps import palettes, get_palette
if out_fig is None:
out_fig = temp_file_path("png")
else:
out_fig = check_file_path(out_fig)
if vis_params is None:
vis_params = {}
elif not isinstance(vis_params, dict):
raise TypeError("The vis_params must be a dictionary.")
if palette is not None:
if palette in ["ndvi", "ndwi", "dem"]:
palette = palettes[palette]
elif palette in list(palettes.keys()):
palette = get_palette(palette)
vis_params["palette"] = palette
orientation = orientation.lower()
if orientation not in ["horizontal", "vertical"]:
raise ValueError("The orientation must be either horizontal or vertical.")
if "opacity" in vis_params:
alpha = vis_params["opacity"]
if type(alpha) not in (int, float):
raise ValueError("The provided opacity value must be type scalar.")
else:
alpha = 1
if "palette" in vis_params:
hexcodes = to_hex_colors(vis_params["palette"])
if discrete:
cmap = mpl.colors.ListedColormap(hexcodes)
vals = np.linspace(vmin, vmax, cmap.N + 1)
norm = mpl.colors.BoundaryNorm(vals, cmap.N)
else:
cmap = mpl.colors.LinearSegmentedColormap.from_list(
"custom", hexcodes, N=256
)
norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
elif cmap is not None:
cmap = mpl.colormaps[cmap]
norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
else:
raise ValueError(
'cmap keyword or "palette" key in vis_params must be provided.'
)
fig, ax = plt.subplots(figsize=(width, height))
cb = mpl.colorbar.ColorbarBase(
ax, norm=norm, alpha=alpha, cmap=cmap, orientation=orientation, **kwargs
)
if label is not None:
cb.set_label(label=label, size=label_size, weight=label_weight)
cb.ax.tick_params(labelsize=tick_size)
if transparent:
bg_color = None
if bg_color is not None:
kwargs["facecolor"] = bg_color
if "bbox_inches" not in kwargs:
kwargs["bbox_inches"] = "tight"
fig.savefig(out_fig, dpi=dpi, transparent=transparent, **kwargs)
if not show_colorbar:
plt.close(fig)
return out_fig
save_data(data, file_ext=None, file_name=None)
¶
Save data in the memory to a file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
data |
object |
The data to be saved. |
required |
file_ext |
str |
The file extension of the file. |
None |
file_name |
str |
The name of the file to be saved. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
str |
The path of the file. |
Source code in leafmap/common.py
def save_data(data, file_ext=None, file_name=None):
"""Save data in the memory to a file.
Args:
data (object): The data to be saved.
file_ext (str): The file extension of the file.
file_name (str, optional): The name of the file to be saved. Defaults to None.
Returns:
str: The path of the file.
"""
import tempfile
import uuid
try:
if file_ext is None:
if hasattr(data, "name"):
_, file_ext = os.path.splitext(data.name)
else:
if not file_ext.startswith("."):
file_ext = "." + file_ext
if file_name is not None:
file_path = os.path.abspath(file_name)
if not file_path.endswith(file_ext):
file_path = file_path + file_ext
else:
file_id = str(uuid.uuid4())
file_path = os.path.join(tempfile.gettempdir(), f"{file_id}{file_ext}")
with open(file_path, "wb") as file:
file.write(data.getbuffer())
return file_path
except Exception as e:
print(e)
return None
screen_capture(outfile, monitor=1)
¶
Takes a full screenshot of the selected monitor.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
outfile |
str |
The output file path to the screenshot. |
required |
monitor |
int |
The monitor to take the screenshot. Defaults to 1. |
1 |
Source code in leafmap/common.py
def screen_capture(outfile, monitor=1):
"""Takes a full screenshot of the selected monitor.
Args:
outfile (str): The output file path to the screenshot.
monitor (int, optional): The monitor to take the screenshot. Defaults to 1.
"""
try:
from mss import mss
except ImportError:
raise ImportError("Please install mss using 'pip install mss'")
out_dir = os.path.dirname(outfile)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
if not isinstance(monitor, int):
print("The monitor number must be an integer.")
return
try:
with mss() as sct:
sct.shot(output=outfile, mon=monitor)
return outfile
except Exception as e:
raise Exception(e)
search_qms(keyword, limit=10, list_only=True, add_prefix=True)
¶
Search for QMS tile providers from Quick Map Services.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
keyword |
str |
The keyword to search for. |
required |
limit |
int |
The maximum number of results to return. Defaults to 10. |
10 |
list_only |
bool |
If True, only the list of services will be returned. Defaults to True. |
True |
add_prefix |
bool |
If True, the prefix "qms." will be added to the service name. Defaults to True. |
True |
Returns:
Type | Description |
---|---|
list |
A list of QMS tile providers. |
Source code in leafmap/common.py
def search_qms(keyword, limit=10, list_only=True, add_prefix=True):
"""Search for QMS tile providers from Quick Map Services.
Args:
keyword (str): The keyword to search for.
limit (int, optional): The maximum number of results to return. Defaults to 10.
list_only (bool, optional): If True, only the list of services will be returned. Defaults to True.
add_prefix (bool, optional): If True, the prefix "qms." will be added to the service name. Defaults to True.
Returns:
list: A list of QMS tile providers.
"""
QMS_API = "https://qms.nextgis.com/api/v1/geoservices"
services = requests.get(
f"{QMS_API}/?search={keyword}&type=tms&epsg=3857&limit={limit}"
)
services = services.json()
if services["results"]:
providers = services["results"]
if list_only:
if add_prefix:
return ["qms." + provider["name"] for provider in providers]
else:
return [provider["name"] for provider in providers]
else:
return providers
else:
return None
search_xyz_services(keyword, name=None, list_only=True, add_prefix=True)
¶
Search for XYZ tile providers from xyzservices.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
keyword |
str |
The keyword to search for. |
required |
name |
str |
The name of the xyz tile. Defaults to None. |
None |
list_only |
bool |
If True, only the list of services will be returned. Defaults to True. |
True |
add_prefix |
bool |
If True, the prefix "xyz." will be added to the service name. Defaults to True. |
True |
Returns:
Type | Description |
---|---|
list |
A list of XYZ tile providers. |
Source code in leafmap/common.py
def search_xyz_services(keyword, name=None, list_only=True, add_prefix=True):
"""Search for XYZ tile providers from xyzservices.
Args:
keyword (str): The keyword to search for.
name (str, optional): The name of the xyz tile. Defaults to None.
list_only (bool, optional): If True, only the list of services will be returned. Defaults to True.
add_prefix (bool, optional): If True, the prefix "xyz." will be added to the service name. Defaults to True.
Returns:
list: A list of XYZ tile providers.
"""
import xyzservices.providers as xyz
if name is None:
providers = xyz.filter(keyword=keyword).flatten()
else:
providers = xyz.filter(name=name).flatten()
if list_only:
if add_prefix:
return ["xyz." + provider for provider in providers]
else:
return [provider for provider in providers]
else:
return providers
select_largest(source, column, count=1, output=None, **kwargs)
¶
Select the largest features in a GeoDataFrame based on a column.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source |
str | gpd.GeoDataFrame |
The path to the vector file or a GeoDataFrame. |
required |
column |
str |
The column to sort by. |
required |
count |
int |
The number of features to select. Defaults to 1. |
1 |
output |
str |
The path to the output vector file. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
str |
The path to the output vector file. |
Source code in leafmap/common.py
def select_largest(source, column, count=1, output=None, **kwargs):
"""Select the largest features in a GeoDataFrame based on a column.
Args:
source (str | gpd.GeoDataFrame): The path to the vector file or a GeoDataFrame.
column (str): The column to sort by.
count (int, optional): The number of features to select. Defaults to 1.
output (str, optional): The path to the output vector file. Defaults to None.
Returns:
str: The path to the output vector file.
"""
import geopandas as gpd
if isinstance(source, str):
gdf = gpd.read_file(source, **kwargs)
else:
gdf = source
if not isinstance(gdf, gpd.GeoDataFrame):
raise TypeError("source must be a GeoDataFrame or a file path")
gdf = gdf.sort_values(column, ascending=False).head(count)
if output is not None:
gdf.to_file(output)
else:
return gdf
set_api_key(key, name='GOOGLE_MAPS_API_KEY')
¶
Sets the Google Maps API key. You can generate one from https://bit.ly/3sw0THG.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
key |
str |
The Google Maps API key. |
required |
name |
str |
The name of the environment variable. Defaults to "GOOGLE_MAPS_API_KEY". |
'GOOGLE_MAPS_API_KEY' |
Source code in leafmap/common.py
def set_api_key(key: str, name: str = "GOOGLE_MAPS_API_KEY"):
"""Sets the Google Maps API key. You can generate one from https://bit.ly/3sw0THG.
Args:
key (str): The Google Maps API key.
name (str, optional): The name of the environment variable. Defaults to "GOOGLE_MAPS_API_KEY".
"""
os.environ[name] = key
set_proxy(port=1080, ip='http://127.0.0.1')
¶
Sets proxy if needed. This is only needed for countries where Google services are not available.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
port |
int |
The proxy port number. Defaults to 1080. |
1080 |
ip |
str |
The IP address. Defaults to 'http://127.0.0.1'. |
'http://127.0.0.1' |
Source code in leafmap/common.py
def set_proxy(
port: Optional[int] = 1080, ip: Optional[str] = "http://127.0.0.1"
) -> None:
"""Sets proxy if needed. This is only needed for countries where Google services are not available.
Args:
port (int, optional): The proxy port number. Defaults to 1080.
ip (str, optional): The IP address. Defaults to 'http://127.0.0.1'.
"""
if not ip.startswith("http://") and not ip.startswith("https://"):
ip = f"http://{ip}"
proxy = f"{ip}:{port}"
os.environ["HTTP_PROXY"] = proxy
os.environ["HTTPS_PROXY"] = proxy
try:
response = requests.get("https://google.com")
response.raise_for_status()
except requests.exceptions.RequestException as e:
print(
"Failed to connect to Google Services. "
"Please double check the port number and IP address."
)
print(f"Error: {e}")
show_html(html)
¶
Shows HTML within Jupyter notebook.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
html |
str |
File path or HTML string. |
required |
Exceptions:
Type | Description |
---|---|
FileNotFoundError |
If the file does not exist. |
Returns:
Type | Description |
---|---|
ipywidgets.HTML |
HTML widget. |
Source code in leafmap/common.py
def show_html(html: str):
"""Shows HTML within Jupyter notebook.
Args:
html (str): File path or HTML string.
Raises:
FileNotFoundError: If the file does not exist.
Returns:
ipywidgets.HTML: HTML widget.
"""
if os.path.exists(html):
with open(html, "r") as f:
content = f.read()
widget = widgets.HTML(value=content)
return widget
else:
try:
widget = widgets.HTML(value=html)
return widget
except Exception as e:
raise Exception(e)
show_image(img_path, width=None, height=None)
¶
Shows an image within Jupyter notebook.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
img_path |
str |
The image file path. |
required |
width |
int |
Width of the image in pixels. Defaults to None. |
None |
height |
int |
Height of the image in pixels. Defaults to None. |
None |
Source code in leafmap/common.py
def show_image(
img_path: str, width: Optional[int] = None, height: Optional[int] = None
):
"""Shows an image within Jupyter notebook.
Args:
img_path (str): The image file path.
width (int, optional): Width of the image in pixels. Defaults to None.
height (int, optional): Height of the image in pixels. Defaults to None.
"""
from IPython.display import display
try:
out = widgets.Output()
# layout={'border': '1px solid black'})
# layout={'border': '1px solid black', 'width': str(width + 20) + 'px', 'height': str(height + 10) + 'px'},)
out.outputs = ()
display(out)
with out:
file = open(img_path, "rb")
image = file.read()
if (width is None) and (height is None):
display(widgets.Image(value=image))
elif (width is not None) and (height is not None):
display(widgets.Image(value=image, width=width, height=height))
else:
print("You need set both width and height.")
return
except Exception as e:
print(e)
show_youtube_video(url, width=800, height=450, allow_autoplay=False, **kwargs)
¶
Displays a Youtube video in a Jupyter notebook.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
url |
string |
a link to a Youtube video. |
required |
width |
int |
the width of the video. Defaults to 800. |
800 |
height |
int |
the height of the video. Defaults to 600. |
450 |
allow_autoplay |
bool |
whether to allow autoplay. Defaults to False. |
False |
**kwargs |
further arguments for IPython.display.YouTubeVideo |
{} |
Returns:
Type | Description |
---|---|
YouTubeVideo |
a video that is displayed in your notebook. |
Source code in leafmap/common.py
def show_youtube_video(url, width=800, height=450, allow_autoplay=False, **kwargs):
"""
Displays a Youtube video in a Jupyter notebook.
Args:
url (string): a link to a Youtube video.
width (int, optional): the width of the video. Defaults to 800.
height (int, optional): the height of the video. Defaults to 600.
allow_autoplay (bool, optional): whether to allow autoplay. Defaults to False.
**kwargs: further arguments for IPython.display.YouTubeVideo
Returns:
YouTubeVideo: a video that is displayed in your notebook.
"""
import re
from IPython.display import YouTubeVideo
if not isinstance(url, str):
raise TypeError("URL must be a string")
match = re.match(
r"^https?:\/\/(?:www\.)?youtube\.com\/watch\?(?=.*v=([^\s&]+)).*$|^https?:\/\/(?:www\.)?youtu\.be\/([^\s&]+).*$",
url,
)
if not match:
raise ValueError("Invalid YouTube video URL")
video_id = match.group(1) if match.group(1) else match.group(2)
return YouTubeVideo(
video_id, width=width, height=height, allow_autoplay=allow_autoplay, **kwargs
)
shp_to_gdf(in_shp)
¶
Converts a shapefile to Geopandas dataframe.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_shp |
str |
File path to the input shapefile. |
required |
Exceptions:
Type | Description |
---|---|
FileNotFoundError |
The provided shp could not be found. |
Returns:
Type | Description |
---|---|
gpd.GeoDataFrame |
geopandas.GeoDataFrame |
Source code in leafmap/common.py
def shp_to_gdf(in_shp):
"""Converts a shapefile to Geopandas dataframe.
Args:
in_shp (str): File path to the input shapefile.
Raises:
FileNotFoundError: The provided shp could not be found.
Returns:
gpd.GeoDataFrame: geopandas.GeoDataFrame
"""
warnings.filterwarnings("ignore")
in_shp = os.path.abspath(in_shp)
if not os.path.exists(in_shp):
raise FileNotFoundError("The provided shp could not be found.")
check_package(name="geopandas", URL="https://geopandas.org")
import geopandas as gpd
try:
return gpd.read_file(in_shp)
except Exception as e:
raise Exception(e)
shp_to_geojson(in_shp, output=None, encoding='utf-8', crs='EPSG:4326', **kwargs)
¶
Converts a shapefile to GeoJSON.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_shp |
str |
File path of the input shapefile. |
required |
output |
str |
File path of the output GeoJSON. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
object |
The json object representing the shapefile. |
Source code in leafmap/common.py
def shp_to_geojson(in_shp, output=None, encoding="utf-8", crs="EPSG:4326", **kwargs):
"""Converts a shapefile to GeoJSON.
Args:
in_shp (str): File path of the input shapefile.
output (str, optional): File path of the output GeoJSON. Defaults to None.
Returns:
object: The json object representing the shapefile.
"""
try:
import geopandas as gpd
gdf = gpd.read_file(in_shp, **kwargs)
gdf.to_crs(crs, inplace=True)
if output is None:
return gdf.__geo_interface__
else:
gdf.to_file(output, driver="GeoJSON")
except Exception as e:
raise Exception(e)
skip_mkdocs_build()
¶
Skips the MkDocs build if the USE_MKDOCS environment variable is set.
Returns:
Type | Description |
---|---|
bool |
Whether to skip the MkDocs build. |
Source code in leafmap/common.py
def skip_mkdocs_build():
"""Skips the MkDocs build if the USE_MKDOCS environment variable is set.
Returns:
bool: Whether to skip the MkDocs build.
"""
if os.environ.get("USE_MKDOCS") is not None:
return True
else:
return False
split_raster(filename, out_dir, tile_size=256, overlap=0, prefix='tile')
¶
Split a raster into tiles.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filename |
str |
The path or http URL to the raster file. |
required |
out_dir |
str |
The path to the output directory. |
required |
tile_size |
int | tuple |
The size of the tiles. Can be an integer or a tuple of (width, height). Defaults to 256. |
256 |
overlap |
int |
The number of pixels to overlap between tiles. Defaults to 0. |
0 |
prefix |
str |
The prefix of the output tiles. Defaults to "tile". |
'tile' |
Exceptions:
Type | Description |
---|---|
ImportError |
Raised if GDAL is not installed. |
Source code in leafmap/common.py
def split_raster(filename, out_dir, tile_size=256, overlap=0, prefix="tile"):
"""Split a raster into tiles.
Args:
filename (str): The path or http URL to the raster file.
out_dir (str): The path to the output directory.
tile_size (int | tuple, optional): The size of the tiles. Can be an integer or a tuple of (width, height). Defaults to 256.
overlap (int, optional): The number of pixels to overlap between tiles. Defaults to 0.
prefix (str, optional): The prefix of the output tiles. Defaults to "tile".
Raises:
ImportError: Raised if GDAL is not installed.
"""
try:
from osgeo import gdal
except ImportError:
raise ImportError(
"GDAL is required to use this function. Install it with `conda install gdal -c conda-forge`"
)
if isinstance(filename, str):
if filename.startswith("http"):
output = filename.split("/")[-1]
download_file(filename, output)
filename = output
# Open the input GeoTIFF file
ds = gdal.Open(filename)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
if isinstance(tile_size, int):
tile_width = tile_size
tile_height = tile_size
elif isinstance(tile_size, tuple):
tile_width = tile_size[0]
tile_height = tile_size[1]
# Get the size of the input raster
width = ds.RasterXSize
height = ds.RasterYSize
# Calculate the number of tiles needed in both directions, taking into account the overlap
num_tiles_x = (width - overlap) // (tile_width - overlap) + int(
(width - overlap) % (tile_width - overlap) > 0
)
num_tiles_y = (height - overlap) // (tile_height - overlap) + int(
(height - overlap) % (tile_height - overlap) > 0
)
# Get the georeferencing information of the input raster
geotransform = ds.GetGeoTransform()
# Loop over all the tiles
for i in range(num_tiles_x):
for j in range(num_tiles_y):
# Calculate the pixel coordinates of the tile, taking into account the overlap and clamping to the edge of the raster
x_min = i * (tile_width - overlap)
y_min = j * (tile_height - overlap)
x_max = min(x_min + tile_width, width)
y_max = min(y_min + tile_height, height)
# Adjust the size of the last tile in each row and column to include any remaining pixels
if i == num_tiles_x - 1:
x_min = max(x_max - tile_width, 0)
if j == num_tiles_y - 1:
y_min = max(y_max - tile_height, 0)
# Calculate the size of the tile, taking into account the overlap
tile_width = x_max - x_min
tile_height = y_max - y_min
# Set the output file name
output_file = f"{out_dir}/{prefix}_{i}_{j}.tif"
# Create a new dataset for the tile
driver = gdal.GetDriverByName("GTiff")
tile_ds = driver.Create(
output_file,
tile_width,
tile_height,
ds.RasterCount,
ds.GetRasterBand(1).DataType,
)
# Calculate the georeferencing information for the output tile
tile_geotransform = (
geotransform[0] + x_min * geotransform[1],
geotransform[1],
0,
geotransform[3] + y_min * geotransform[5],
0,
geotransform[5],
)
# Set the geotransform and projection of the tile
tile_ds.SetGeoTransform(tile_geotransform)
tile_ds.SetProjection(ds.GetProjection())
# Read the data from the input raster band(s) and write it to the tile band(s)
for k in range(ds.RasterCount):
band = ds.GetRasterBand(k + 1)
tile_band = tile_ds.GetRasterBand(k + 1)
tile_data = band.ReadAsArray(x_min, y_min, tile_width, tile_height)
tile_band.WriteArray(tile_data)
# Close the tile dataset
tile_ds = None
# Close the input dataset
ds = None
st_download_button(label, data, file_name=None, mime=None, key=None, help=None, on_click=None, args=None, csv_sep=',', **kwargs)
¶
Streamlit function to create a download button.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
label |
str |
A short label explaining to the user what this button is for.. |
required |
data |
str | list |
The contents of the file to be downloaded. See example below for caching techniques to avoid recomputing this data unnecessarily. |
required |
file_name |
str |
An optional string to use as the name of the file to be downloaded, such as 'my_file.csv'. If not specified, the name will be automatically generated. Defaults to None. |
None |
mime |
str |
The MIME type of the data. If None, defaults to "text/plain" (if data is of type str or is a textual file) or "application/octet-stream" (if data is of type bytes or is a binary file). Defaults to None. |
None |
key |
str |
An optional string or integer to use as the unique key for the widget. If this is omitted, a key will be generated for the widget based on its content. Multiple widgets of the same type may not share the same key. Defaults to None. |
None |
help |
str |
An optional tooltip that gets displayed when the button is hovered over. Defaults to None. |
None |
on_click |
str |
An optional callback invoked when this button is clicked. Defaults to None. |
None |
args |
list |
An optional tuple of args to pass to the callback. Defaults to None. |
None |
kwargs |
dict |
An optional tuple of args to pass to the callback. |
{} |
Source code in leafmap/common.py
def st_download_button(
label,
data,
file_name=None,
mime=None,
key=None,
help=None,
on_click=None,
args=None,
csv_sep=",",
**kwargs,
):
"""Streamlit function to create a download button.
Args:
label (str): A short label explaining to the user what this button is for..
data (str | list): The contents of the file to be downloaded. See example below for caching techniques to avoid recomputing this data unnecessarily.
file_name (str, optional): An optional string to use as the name of the file to be downloaded, such as 'my_file.csv'. If not specified, the name will be automatically generated. Defaults to None.
mime (str, optional): The MIME type of the data. If None, defaults to "text/plain" (if data is of type str or is a textual file) or "application/octet-stream" (if data is of type bytes or is a binary file). Defaults to None.
key (str, optional): An optional string or integer to use as the unique key for the widget. If this is omitted, a key will be generated for the widget based on its content. Multiple widgets of the same type may not share the same key. Defaults to None.
help (str, optional): An optional tooltip that gets displayed when the button is hovered over. Defaults to None.
on_click (str, optional): An optional callback invoked when this button is clicked. Defaults to None.
args (list, optional): An optional tuple of args to pass to the callback. Defaults to None.
kwargs (dict, optional): An optional tuple of args to pass to the callback.
"""
try:
import streamlit as st
import pandas as pd
if isinstance(data, str):
if file_name is None:
file_name = data.split("/")[-1]
if data.endswith(".csv"):
data = pd.read_csv(data).to_csv(sep=csv_sep, index=False)
if mime is None:
mime = "text/csv"
return st.download_button(
label, data, file_name, mime, key, help, on_click, args, **kwargs
)
elif (
data.endswith(".gif") or data.endswith(".png") or data.endswith(".jpg")
):
if mime is None:
mime = f"image/{os.path.splitext(data)[1][1:]}"
with open(data, "rb") as file:
return st.download_button(
label,
file,
file_name,
mime,
key,
help,
on_click,
args,
**kwargs,
)
elif isinstance(data, pd.DataFrame):
if file_name is None:
file_name = "data.csv"
data = data.to_csv(sep=csv_sep, index=False)
if mime is None:
mime = "text/csv"
return st.download_button(
label, data, file_name, mime, key, help, on_click, args, **kwargs
)
else:
# if mime is None:
# mime = "application/pdf"
return st.download_button(
label,
data,
file_name,
mime,
key,
help,
on_click,
args,
**kwargs,
)
except ImportError:
print("Streamlit is not installed. Please run 'pip install streamlit'.")
return
except Exception as e:
raise Exception(e)
start_server(directory=None, port=8000, background=True, quiet=True)
¶
Start a simple web server to serve files from the specified directory with directory listing and CORS support. Optionally, run the server asynchronously in a background thread.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
directory |
str |
The directory from which files will be served. |
None |
port |
int |
The port on which the web server will run. Defaults to 8000. |
8000 |
background |
bool |
Whether to run the server in a separate background thread. Defaults to True. |
True |
quiet |
bool |
If True, suppress the log output. Defaults to True. |
True |
Exceptions:
Type | Description |
---|---|
ImportError |
If required modules are not found. |
Exception |
Catches other unexpected errors during execution. |
Returns:
Type | Description |
---|---|
None |
None. The function runs the server indefinitely until manually stopped. |
Source code in leafmap/common.py
def start_server(
directory: str = None, port: int = 8000, background: bool = True, quiet: bool = True
) -> None:
"""
Start a simple web server to serve files from the specified directory
with directory listing and CORS support. Optionally, run the server
asynchronously in a background thread.
Args:
directory (str): The directory from which files will be served.
port (int, optional): The port on which the web server will run. Defaults to 8000.
background (bool, optional): Whether to run the server in a separate background thread.
Defaults to True.
quiet (bool, optional): If True, suppress the log output. Defaults to True.
Raises:
ImportError: If required modules are not found.
Exception: Catches other unexpected errors during execution.
Returns:
None. The function runs the server indefinitely until manually stopped.
"""
# If no directory is specified, use the current working directory
if directory is None:
directory = os.getcwd()
def run_flask():
try:
from flask import Flask, send_from_directory, render_template_string
from flask_cors import CORS
app = Flask(__name__, static_folder=directory)
CORS(app) # Enable CORS for all routes
if quiet:
# This will disable Flask's logging
import logging
log = logging.getLogger("werkzeug")
log.disabled = True
app.logger.disabled = True
@app.route("/<path:path>", methods=["GET"])
def serve_file(path):
return send_from_directory(directory, path)
@app.route("/", methods=["GET"])
def index():
# List files and directories under the specified directory
items = os.listdir(directory)
items.sort()
# Generate an HTML representation of the directory listing
listing_template = """
<h2>Directory listing for /</h2>
<hr>
<ul>
{% for item in items %}
<li><a href="{{ item }}">{{ item }}</a></li>
{% endfor %}
</ul>
"""
return render_template_string(listing_template, items=items)
print(f"Server is running at http://127.0.0.1:{port}/")
app.run(port=port)
except ImportError as e:
print(f"Error importing module: {e}")
except Exception as e:
print(f"An error occurred: {e}")
if background:
import threading
# Start the Flask server in a new background thread
t = threading.Thread(target=run_flask)
t.start()
else:
# Run the Flask server in the main thread
run_flask()
streamlit_legend(html, width=None, height=None, scrolling=True)
¶
Streamlit function to display a legend.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
html |
str |
The HTML string of the legend. |
required |
width |
str |
The width of the legend. Defaults to None. |
None |
height |
str |
The height of the legend. Defaults to None. |
None |
scrolling |
bool |
Whether to allow scrolling in the legend. Defaults to True. |
True |
Source code in leafmap/common.py
def streamlit_legend(html, width=None, height=None, scrolling=True):
"""Streamlit function to display a legend.
Args:
html (str): The HTML string of the legend.
width (str, optional): The width of the legend. Defaults to None.
height (str, optional): The height of the legend. Defaults to None.
scrolling (bool, optional): Whether to allow scrolling in the legend. Defaults to True.
"""
try:
import streamlit.components.v1 as components
components.html(html, width=width, height=height, scrolling=scrolling)
except ImportError:
print("Streamlit is not installed. Please run 'pip install streamlit'.")
return
system_fonts(show_full_path=False)
¶
Gets a list of system fonts
1 2 3 4 |
|
Parameters:
Name | Type | Description | Default |
---|---|---|---|
show_full_path |
bool |
Whether to show the full path of each system font. Defaults to False. |
False |
Returns:
Type | Description |
---|---|
list |
A list of system fonts. |
Source code in leafmap/common.py
def system_fonts(show_full_path: Optional[bool] = False) -> List:
"""Gets a list of system fonts
# Common font locations:
# Linux: /usr/share/fonts/TTF/
# Windows: C:/Windows/Fonts
# macOS: System > Library > Fonts
Args:
show_full_path (bool, optional): Whether to show the full path of each system font. Defaults to False.
Returns:
list: A list of system fonts.
"""
try:
import matplotlib.font_manager
font_list = matplotlib.font_manager.findSystemFonts(
fontpaths=None, fontext="ttf"
)
font_list.sort()
font_names = [os.path.basename(f) for f in font_list]
font_names.sort()
if show_full_path:
return font_list
else:
return font_names
except Exception as e:
raise Exception(e)
temp_file_path(extension)
¶
Returns a temporary file path.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
extension |
str |
The file extension. |
required |
Returns:
Type | Description |
---|---|
str |
The temporary file path. |
Source code in leafmap/common.py
def temp_file_path(extension):
"""Returns a temporary file path.
Args:
extension (str): The file extension.
Returns:
str: The temporary file path.
"""
import tempfile
import uuid
if not extension.startswith("."):
extension = "." + extension
file_id = str(uuid.uuid4())
file_path = os.path.join(tempfile.gettempdir(), f"{file_id}{extension}")
return file_path
tif_to_jp2(filename, output, creationOptions=None)
¶
Converts a GeoTIFF to JPEG2000.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filename |
str |
The path to the GeoTIFF file. |
required |
output |
str |
The path to the output JPEG2000 file. |
required |
creationOptions |
list |
A list of creation options for the JPEG2000 file. See
https://gdal.org/drivers/raster/jp2openjpeg.html. For example, to specify the compression
ratio, use |
None |
Source code in leafmap/common.py
def tif_to_jp2(filename, output, creationOptions=None):
"""Converts a GeoTIFF to JPEG2000.
Args:
filename (str): The path to the GeoTIFF file.
output (str): The path to the output JPEG2000 file.
creationOptions (list): A list of creation options for the JPEG2000 file. See
https://gdal.org/drivers/raster/jp2openjpeg.html. For example, to specify the compression
ratio, use ``["QUALITY=20"]``. A value of 20 means the file will be 20% of the size in comparison
to uncompressed data.
"""
if not os.path.exists(filename):
raise Exception(f"File {filename} does not exist")
if not output.endswith(".jp2"):
output += ".jp2"
from osgeo import gdal
in_ds = gdal.Open(filename)
gdal.Translate(output, in_ds, format="JP2OpenJPEG", creationOptions=creationOptions)
in_ds = None
tms_to_geotiff(output, bbox, zoom=None, resolution=None, source='OpenStreetMap', crs='EPSG:3857', to_cog=False, quiet=False, **kwargs)
¶
Download map tiles and convert them to a GeoTIFF. The source is adapted from https://github.com/gumblex/tms2geotiff. Credits to the GitHub user @gumblex.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
output |
str |
The output GeoTIFF file. |
required |
bbox |
list |
The bounding box [minx, miny, maxx, maxy] coordinates in EPSG:4326, e.g., [-122.5216, 37.733, -122.3661, 37.8095] |
required |
zoom |
int |
The map zoom level. Defaults to None. |
None |
resolution |
float |
The resolution in meters. Defaults to None. |
None |
source |
str |
The tile source. It can be one of the following: "OPENSTREETMAP", "ROADMAP", "SATELLITE", "TERRAIN", "HYBRID", or an HTTP URL. Defaults to "OpenStreetMap". |
'OpenStreetMap' |
crs |
str |
The coordinate reference system. Defaults to "EPSG:3857". |
'EPSG:3857' |
to_cog |
bool |
Convert to Cloud Optimized GeoTIFF. Defaults to False. |
False |
quiet |
bool |
Suppress output. Defaults to False. |
False |
**kwargs |
Additional arguments to pass to gdal.GetDriverByName("GTiff").Create(). |
{} |
Source code in leafmap/common.py
def map_tiles_to_geotiff(
output,
bbox,
zoom=None,
resolution=None,
source="OpenStreetMap",
crs="EPSG:3857",
to_cog=False,
quiet=False,
**kwargs,
):
"""Download map tiles and convert them to a GeoTIFF. The source is adapted from https://github.com/gumblex/tms2geotiff.
Credits to the GitHub user @gumblex.
Args:
output (str): The output GeoTIFF file.
bbox (list): The bounding box [minx, miny, maxx, maxy] coordinates in EPSG:4326, e.g., [-122.5216, 37.733, -122.3661, 37.8095]
zoom (int, optional): The map zoom level. Defaults to None.
resolution (float, optional): The resolution in meters. Defaults to None.
source (str, optional): The tile source. It can be one of the following: "OPENSTREETMAP", "ROADMAP",
"SATELLITE", "TERRAIN", "HYBRID", or an HTTP URL. Defaults to "OpenStreetMap".
crs (str, optional): The coordinate reference system. Defaults to "EPSG:3857".
to_cog (bool, optional): Convert to Cloud Optimized GeoTIFF. Defaults to False.
quiet (bool, optional): Suppress output. Defaults to False.
**kwargs: Additional arguments to pass to gdal.GetDriverByName("GTiff").Create().
"""
import re
import io
import math
import itertools
import concurrent.futures
import numpy
from PIL import Image
try:
from osgeo import gdal, osr
except ImportError:
raise ImportError("GDAL is not installed. Install it with pip install GDAL")
try:
import httpx
SESSION = httpx.Client()
except ImportError:
import requests
SESSION = requests.Session()
SESSION.headers.update(
{
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0",
}
)
xyz_tiles = {
"OPENSTREETMAP": {
"url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
"attribution": "OpenStreetMap",
"name": "OpenStreetMap",
},
"ROADMAP": {
"url": "https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}",
"attribution": "Google",
"name": "Google Maps",
},
"SATELLITE": {
"url": "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}",
"attribution": "Google",
"name": "Google Satellite",
},
"TERRAIN": {
"url": "https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}",
"attribution": "Google",
"name": "Google Terrain",
},
"HYBRID": {
"url": "https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}",
"attribution": "Google",
"name": "Google Satellite",
},
}
if isinstance(source, str) and source.upper() in xyz_tiles:
source = xyz_tiles[source.upper()]["url"]
elif isinstance(source, str) and source.startswith("http"):
pass
elif isinstance(source, str):
tiles = basemap_xyz_tiles()
if source in tiles:
source = tiles[source].url
else:
raise ValueError(
'source must be one of "OpenStreetMap", "ROADMAP", "SATELLITE", "TERRAIN", "HYBRID", or a URL'
)
def resolution_to_zoom_level(resolution):
"""
Convert map resolution in meters to zoom level for Web Mercator (EPSG:3857) tiles.
"""
# Web Mercator tile size in meters at zoom level 0
initial_resolution = 156543.03392804097
# Calculate the zoom level
zoom_level = math.log2(initial_resolution / resolution)
return int(zoom_level)
if isinstance(bbox, list) and len(bbox) == 4:
west, south, east, north = bbox
else:
raise ValueError(
"bbox must be a list of 4 coordinates in the format of [xmin, ymin, xmax, ymax]"
)
if zoom is None and resolution is None:
raise ValueError("Either zoom or resolution must be provided")
elif zoom is not None and resolution is not None:
raise ValueError("Only one of zoom or resolution can be provided")
if resolution is not None:
zoom = resolution_to_zoom_level(resolution)
EARTH_EQUATORIAL_RADIUS = 6378137.0
Image.MAX_IMAGE_PIXELS = None
gdal.UseExceptions()
web_mercator = osr.SpatialReference()
try:
web_mercator.ImportFromEPSG(3857)
except RuntimeError as e:
# https://github.com/PDAL/PDAL/issues/2544#issuecomment-637995923
if "PROJ" in str(e):
pattern = r"/[\w/]+"
match = re.search(pattern, str(e))
if match:
file_path = match.group(0)
os.environ["PROJ_LIB"] = file_path
os.environ["GDAL_DATA"] = file_path.replace("proj", "gdal")
web_mercator.ImportFromEPSG(3857)
WKT_3857 = web_mercator.ExportToWkt()
def from4326_to3857(lat, lon):
xtile = math.radians(lon) * EARTH_EQUATORIAL_RADIUS
ytile = (
math.log(math.tan(math.radians(45 + lat / 2.0))) * EARTH_EQUATORIAL_RADIUS
)
return (xtile, ytile)
def deg2num(lat, lon, zoom):
lat_r = math.radians(lat)
n = 2**zoom
xtile = (lon + 180) / 360 * n
ytile = (1 - math.log(math.tan(lat_r) + 1 / math.cos(lat_r)) / math.pi) / 2 * n
return (xtile, ytile)
def is_empty(im):
extrema = im.getextrema()
if len(extrema) >= 3:
if len(extrema) > 3 and extrema[-1] == (0, 0):
return True
for ext in extrema[:3]:
if ext != (0, 0):
return False
return True
else:
return extrema[0] == (0, 0)
def paste_tile(bigim, base_size, tile, corner_xy, bbox):
if tile is None:
return bigim
im = Image.open(io.BytesIO(tile))
mode = "RGB" if im.mode == "RGB" else "RGBA"
size = im.size
if bigim is None:
base_size[0] = size[0]
base_size[1] = size[1]
newim = Image.new(
mode, (size[0] * (bbox[2] - bbox[0]), size[1] * (bbox[3] - bbox[1]))
)
else:
newim = bigim
dx = abs(corner_xy[0] - bbox[0])
dy = abs(corner_xy[1] - bbox[1])
xy0 = (size[0] * dx, size[1] * dy)
if mode == "RGB":
newim.paste(im, xy0)
else:
if im.mode != mode:
im = im.convert(mode)
if not is_empty(im):
newim.paste(im, xy0)
im.close()
return newim
def finish_picture(bigim, base_size, bbox, x0, y0, x1, y1):
xfrac = x0 - bbox[0]
yfrac = y0 - bbox[1]
x2 = round(base_size[0] * xfrac)
y2 = round(base_size[1] * yfrac)
imgw = round(base_size[0] * (x1 - x0))
imgh = round(base_size[1] * (y1 - y0))
retim = bigim.crop((x2, y2, x2 + imgw, y2 + imgh))
if retim.mode == "RGBA" and retim.getextrema()[3] == (255, 255):
retim = retim.convert("RGB")
bigim.close()
return retim
def get_tile(url):
retry = 3
while 1:
try:
r = SESSION.get(url, timeout=60)
break
except Exception:
retry -= 1
if not retry:
raise
if r.status_code == 404:
return None
elif not r.content:
return None
r.raise_for_status()
return r.content
def draw_tile(
source, lat0, lon0, lat1, lon1, zoom, filename, quiet=False, **kwargs
):
x0, y0 = deg2num(lat0, lon0, zoom)
x1, y1 = deg2num(lat1, lon1, zoom)
x0, x1 = sorted([x0, x1])
y0, y1 = sorted([y0, y1])
corners = tuple(
itertools.product(
range(math.floor(x0), math.ceil(x1)),
range(math.floor(y0), math.ceil(y1)),
)
)
totalnum = len(corners)
futures = []
with concurrent.futures.ThreadPoolExecutor(5) as executor:
for x, y in corners:
futures.append(
executor.submit(get_tile, source.format(z=zoom, x=x, y=y))
)
bbox = (math.floor(x0), math.floor(y0), math.ceil(x1), math.ceil(y1))
bigim = None
base_size = [256, 256]
for k, (fut, corner_xy) in enumerate(zip(futures, corners), 1):
bigim = paste_tile(bigim, base_size, fut.result(), corner_xy, bbox)
if not quiet:
print("Downloaded image %d/%d" % (k, totalnum))
if not quiet:
print("Saving GeoTIFF. Please wait...")
img = finish_picture(bigim, base_size, bbox, x0, y0, x1, y1)
imgbands = len(img.getbands())
driver = gdal.GetDriverByName("GTiff")
if "options" not in kwargs:
kwargs["options"] = [
"COMPRESS=DEFLATE",
"PREDICTOR=2",
"ZLEVEL=9",
"TILED=YES",
]
kwargs.pop("overwrite", None)
gtiff = driver.Create(
filename,
img.size[0],
img.size[1],
imgbands,
gdal.GDT_Byte,
**kwargs,
)
xp0, yp0 = from4326_to3857(lat0, lon0)
xp1, yp1 = from4326_to3857(lat1, lon1)
pwidth = abs(xp1 - xp0) / img.size[0]
pheight = abs(yp1 - yp0) / img.size[1]
gtiff.SetGeoTransform((min(xp0, xp1), pwidth, 0, max(yp0, yp1), 0, -pheight))
gtiff.SetProjection(WKT_3857)
for band in range(imgbands):
array = np.array(img.getdata(band), dtype="u8")
array = array.reshape((img.size[1], img.size[0]))
band = gtiff.GetRasterBand(band + 1)
band.WriteArray(array)
gtiff.FlushCache()
if not quiet:
print(f"Image saved to {filename}")
return img
try:
draw_tile(source, south, west, north, east, zoom, output, quiet, **kwargs)
if crs.upper() != "EPSG:3857":
reproject(output, output, crs, to_cog=to_cog)
elif to_cog:
image_to_cog(output, output)
except Exception as e:
raise Exception(e)
to_hex_colors(colors)
¶
Adds # to a list of hex color codes.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
colors |
list |
A list of hex color codes. |
required |
Returns:
Type | Description |
---|---|
list |
A list of hex color codes prefixed with #. |
Source code in leafmap/common.py
def to_hex_colors(colors):
"""Adds # to a list of hex color codes.
Args:
colors (list): A list of hex color codes.
Returns:
list: A list of hex color codes prefixed with #.
"""
result = all([len(color.strip()) == 6 for color in colors])
if result:
return ["#" + color.strip() for color in colors]
else:
return colors
transform_bbox_coords(bbox, src_crs, dst_crs, **kwargs)
¶
Transforms the coordinates of a bounding box [x1, y1, x2, y2] from one CRS to another.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
bbox |
list | tuple |
The bounding box [x1, y1, x2, y2] coordinates. |
required |
src_crs |
str |
The source CRS, e.g., "EPSG:4326". |
required |
dst_crs |
str |
The destination CRS, e.g., "EPSG:3857". |
required |
Returns:
Type | Description |
---|---|
list |
The transformed bounding box [x1, y1, x2, y2] coordinates. |
Source code in leafmap/common.py
def transform_bbox_coords(bbox, src_crs, dst_crs, **kwargs):
"""Transforms the coordinates of a bounding box [x1, y1, x2, y2] from one CRS to another.
Args:
bbox (list | tuple): The bounding box [x1, y1, x2, y2] coordinates.
src_crs (str): The source CRS, e.g., "EPSG:4326".
dst_crs (str): The destination CRS, e.g., "EPSG:3857".
Returns:
list: The transformed bounding box [x1, y1, x2, y2] coordinates.
"""
x1, y1, x2, y2 = bbox
x1, y1 = transform_coords(
x1, y1, src_crs, dst_crs, **kwargs
) # pylint: disable=E0633
x2, y2 = transform_coords(
x2, y2, src_crs, dst_crs, **kwargs
) # pylint: disable=E0633
return [x1, y1, x2, y2]
transform_coords(x, y, src_crs, dst_crs, **kwargs)
¶
Transform coordinates from one CRS to another.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
x |
float |
The x coordinate. |
required |
y |
float |
The y coordinate. |
required |
src_crs |
str |
The source CRS, e.g., "EPSG:4326". |
required |
dst_crs |
str |
The destination CRS, e.g., "EPSG:3857". |
required |
Returns:
Type | Description |
---|---|
dict |
The transformed coordinates in the format of (x, y) |
Source code in leafmap/common.py
def transform_coords(x, y, src_crs, dst_crs, **kwargs):
"""Transform coordinates from one CRS to another.
Args:
x (float): The x coordinate.
y (float): The y coordinate.
src_crs (str): The source CRS, e.g., "EPSG:4326".
dst_crs (str): The destination CRS, e.g., "EPSG:3857".
Returns:
dict: The transformed coordinates in the format of (x, y)
"""
import pyproj
transformer = pyproj.Transformer.from_crs(
src_crs, dst_crs, always_xy=True, **kwargs
)
return transformer.transform(x, y)
update_package()
¶
Updates the leafmap package from the leafmap GitHub repository without the need to use pip or conda. In this way, I don't have to keep updating pypi and conda-forge with every minor update of the package.
Source code in leafmap/common.py
def update_package() -> None:
"""Updates the leafmap package from the leafmap GitHub repository without the need to use pip or conda.
In this way, I don't have to keep updating pypi and conda-forge with every minor update of the package.
"""
download_dir = Path.home() / "Downloads"
download_dir.mkdir(parents=True, exist_ok=True)
_clone_repo(out_dir=str(download_dir))
pkg_dir = download_dir / "leafmap-master"
work_dir = Path.cwd()
os.chdir(pkg_dir)
try:
if shutil.which("pip"):
cmd = ["pip", "install", "."]
else:
cmd = ["pip3", "install", "."]
subprocess.run(cmd, check=True)
except subprocess.CalledProcessError as error:
print(f"Failed to install the package: {error}")
finally:
os.chdir(work_dir)
print(
"\nPlease comment out 'leafmap.update_package()' and restart kernel to take effect:\n"
"Jupyter menu -> Kernel -> Restart & Clear Output"
)
upload_to_imgur(in_gif)
¶
Uploads an image to imgur.com
Parameters:
Name | Type | Description | Default |
---|---|---|---|
in_gif |
str |
The file path to the image. |
required |
Source code in leafmap/common.py
def upload_to_imgur(in_gif: str):
"""Uploads an image to imgur.com
Args:
in_gif (str): The file path to the image.
"""
import subprocess
pkg_name = "imgur-uploader"
if not _is_tool(pkg_name):
_check_install(pkg_name)
try:
IMGUR_API_ID = os.environ.get("IMGUR_API_ID", None)
IMGUR_API_SECRET = os.environ.get("IMGUR_API_SECRET", None)
credentials_path = os.path.join(
os.path.expanduser("~"), ".config/imgur_uploader/uploader.cfg"
)
if (
(IMGUR_API_ID is not None) and (IMGUR_API_SECRET is not None)
) or os.path.exists(credentials_path):
proc = subprocess.Popen(["imgur-uploader", in_gif], stdout=subprocess.PIPE)
for _ in range(0, 2):
line = proc.stdout.readline()
print(line.rstrip().decode("utf-8"))
# while True:
# line = proc.stdout.readline()
# if not line:
# break
# print(line.rstrip().decode("utf-8"))
else:
print(
"Imgur API credentials could not be found. Please check https://pypi.org/project/imgur-uploader/ for instructions on how to get Imgur API credentials"
)
return
except Exception as e:
raise Exception(e)
vector_area(vector, unit='m2', crs='epsg:3857')
¶
Calculate the area of a vector.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
vector |
str |
A local path or HTTP URL to a vector. |
required |
unit |
str |
The unit of the area, can be 'm2', 'km2', 'ha', or 'acres'. Defaults to 'm2'. |
'm2' |
Returns:
Type | Description |
---|---|
float |
The area of the vector. |
Source code in leafmap/common.py
def vector_area(vector, unit="m2", crs="epsg:3857"):
"""Calculate the area of a vector.
Args:
vector (str): A local path or HTTP URL to a vector.
unit (str, optional): The unit of the area, can be 'm2', 'km2', 'ha', or 'acres'. Defaults to 'm2'.
Returns:
float: The area of the vector.
"""
import geopandas as gpd
if isinstance(vector, str):
gdf = gpd.read_file(vector)
elif isinstance(vector, gpd.GeoDataFrame):
gdf = vector
area = gdf.to_crs(crs).area.sum()
if unit == "m2":
return area
elif unit == "km2":
return area / 1000000
elif unit == "ha":
return area / 10000
elif unit == "acres":
return area / 4046.8564224
else:
raise ValueError("Invalid unit.")
vector_col_names(filename, **kwargs)
¶
Retrieves the column names of a vector attribute table.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filename |
str |
The input file path. |
required |
Returns:
Type | Description |
---|---|
list |
The list of column names. |
Source code in leafmap/common.py
def vector_col_names(filename, **kwargs):
"""Retrieves the column names of a vector attribute table.
Args:
filename (str): The input file path.
Returns:
list: The list of column names.
"""
warnings.filterwarnings("ignore")
check_package(name="geopandas", URL="https://geopandas.org")
import geopandas as gpd
import fiona
if not filename.startswith("http"):
filename = os.path.abspath(filename)
ext = os.path.splitext(filename)[1].lower()
if ext == ".kml":
fiona.drvsupport.supported_drivers["KML"] = "rw"
gdf = gpd.read_file(filename, driver="KML", **kwargs)
else:
gdf = gpd.read_file(filename, **kwargs)
col_names = gdf.columns.values.tolist()
return col_names
vector_geom_type(data, first_only=True, **kwargs)
¶
Returns the geometry type of a vector dataset.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
gdf |
gpd.GeoDataFrame |
A GeoDataFrame. |
required |
first_only |
bool |
Whether to return the geometry type of the first feature in the GeoDataFrame. Defaults to True. |
True |
kwargs |
Additional keyword arguments to pass to the geopandas.read_file function. |
{} |
Returns:
Type | Description |
---|---|
str |
The geometry type of the GeoDataFrame, such as Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon. For more info, see https://shapely.readthedocs.io/en/stable/manual.html |
Source code in leafmap/common.py
def vector_geom_type(data, first_only=True, **kwargs):
"""Returns the geometry type of a vector dataset.
Args:
gdf (gpd.GeoDataFrame): A GeoDataFrame.
first_only (bool, optional): Whether to return the geometry type of the
first feature in the GeoDataFrame. Defaults to True.
kwargs: Additional keyword arguments to pass to the geopandas.read_file function.
Returns:
str: The geometry type of the GeoDataFrame, such as Point, LineString,
Polygon, MultiPoint, MultiLineString, MultiPolygon.
For more info, see https://shapely.readthedocs.io/en/stable/manual.html
"""
import geopandas as gpd
if isinstance(data, str) or isinstance(data, dict):
gdf = gpd.read_file(data, **kwargs)
if first_only:
return gdf.geometry.type[0]
else:
return gdf.geometry.type
vector_set_crs(source, output=None, crs='EPSG:4326', **kwargs)
¶
Set CRS of a vector file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source |
str | gpd.GeoDataFrame |
The path to the vector file or a GeoDataFrame. |
required |
output |
str |
The path to the output vector file. Defaults to None. |
None |
crs |
str |
The CRS to set. Defaults to "EPSG:4326". |
'EPSG:4326' |
Returns:
Type | Description |
---|---|
gpd.GeoDataFrame |
The GeoDataFrame with the new CRS. |
Source code in leafmap/common.py
def vector_set_crs(source, output=None, crs="EPSG:4326", **kwargs):
"""Set CRS of a vector file.
Args:
source (str | gpd.GeoDataFrame): The path to the vector file or a GeoDataFrame.
output (str, optional): The path to the output vector file. Defaults to None.
crs (str, optional): The CRS to set. Defaults to "EPSG:4326".
Returns:
gpd.GeoDataFrame: The GeoDataFrame with the new CRS.
"""
import geopandas as gpd
if isinstance(source, str):
source = gpd.read_file(source, **kwargs)
if not isinstance(source, gpd.GeoDataFrame):
raise TypeError("source must be a GeoDataFrame or a file path")
gdf = source.set_crs(crs)
if output is not None:
gdf.to_file(output)
else:
return gdf
vector_to_geojson(filename, out_geojson=None, bbox=None, mask=None, rows=None, epsg='4326', encoding='utf-8', **kwargs)
¶
Converts any geopandas-supported vector dataset to GeoJSON.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filename |
str |
Either the absolute or relative path to the file or URL to be opened, or any object with a read() method (such as an open file or StringIO). |
required |
out_geojson |
str |
The file path to the output GeoJSON. Defaults to None. |
None |
bbox |
tuple | GeoDataFrame or GeoSeries | shapely Geometry |
Filter features by given bounding box, GeoSeries, GeoDataFrame or a shapely geometry. CRS mis-matches are resolved if given a GeoSeries or GeoDataFrame. Cannot be used with mask. Defaults to None. |
None |
mask |
dict | GeoDataFrame or GeoSeries | shapely Geometry |
Filter for features that intersect with the given dict-like geojson geometry, GeoSeries, GeoDataFrame or shapely geometry. CRS mis-matches are resolved if given a GeoSeries or GeoDataFrame. Cannot be used with bbox. Defaults to None. |
None |
rows |
int or slice |
Load in specific rows by passing an integer (first n rows) or a slice() object.. Defaults to None. |
None |
epsg |
str |
The EPSG number to convert to. Defaults to "4326". |
'4326' |
encoding |
str |
The encoding of the input file. Defaults to "utf-8". |
'utf-8' |
Exceptions:
Type | Description |
---|---|
ValueError |
When the output file path is invalid. |
Returns:
Type | Description |
---|---|
dict |
A dictionary containing the GeoJSON. |
Source code in leafmap/common.py
def vector_to_geojson(
filename,
out_geojson=None,
bbox=None,
mask=None,
rows=None,
epsg="4326",
encoding="utf-8",
**kwargs,
):
"""Converts any geopandas-supported vector dataset to GeoJSON.
Args:
filename (str): Either the absolute or relative path to the file or URL to be opened, or any object with a read() method (such as an open file or StringIO).
out_geojson (str, optional): The file path to the output GeoJSON. Defaults to None.
bbox (tuple | GeoDataFrame or GeoSeries | shapely Geometry, optional): Filter features by given bounding box, GeoSeries, GeoDataFrame or a shapely geometry. CRS mis-matches are resolved if given a GeoSeries or GeoDataFrame. Cannot be used with mask. Defaults to None.
mask (dict | GeoDataFrame or GeoSeries | shapely Geometry, optional): Filter for features that intersect with the given dict-like geojson geometry, GeoSeries, GeoDataFrame or shapely geometry. CRS mis-matches are resolved if given a GeoSeries or GeoDataFrame. Cannot be used with bbox. Defaults to None.
rows (int or slice, optional): Load in specific rows by passing an integer (first n rows) or a slice() object.. Defaults to None.
epsg (str, optional): The EPSG number to convert to. Defaults to "4326".
encoding (str, optional): The encoding of the input file. Defaults to "utf-8".
Raises:
ValueError: When the output file path is invalid.
Returns:
dict: A dictionary containing the GeoJSON.
"""
warnings.filterwarnings("ignore")
check_package(name="geopandas", URL="https://geopandas.org")
import geopandas as gpd
import fiona
if not filename.startswith("http"):
filename = os.path.abspath(filename)
if filename.endswith(".zip"):
filename = "zip://" + filename
ext = os.path.splitext(filename)[1].lower()
if ext == ".kml":
fiona.drvsupport.supported_drivers["KML"] = "rw"
df = gpd.read_file(
filename,
bbox=bbox,
mask=mask,
rows=rows,
driver="KML",
encoding=encoding,
**kwargs,
)
else:
df = gpd.read_file(
filename, bbox=bbox, mask=mask, rows=rows, encoding=encoding, **kwargs
)
gdf = df.to_crs(epsg=epsg)
if out_geojson is not None:
if not out_geojson.lower().endswith(".geojson"):
raise ValueError("The output file must have a geojson file extension.")
out_geojson = os.path.abspath(out_geojson)
out_dir = os.path.dirname(out_geojson)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
gdf.to_file(out_geojson, driver="GeoJSON")
else:
return gdf.__geo_interface__
vector_to_gif(filename, out_gif, colname, vmin=None, vmax=None, step=1, facecolor='black', figsize=(10, 8), padding=3, title=None, add_text=True, xy=('1%', '1%'), fontsize=20, add_progress_bar=True, progress_bar_color='blue', progress_bar_height=5, dpi=300, fps=10, loop=0, mp4=False, keep_png=False, verbose=True, open_args={}, plot_args={})
¶
Convert a vector to a gif. This function was inspired by by Johannes Uhl's shapefile2gif repo at https://github.com/johannesuhl/shapefile2gif. Credits to Johannes Uhl.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filename |
str |
The input vector file. Can be a directory path or http URL, e.g., "https://i.imgur.com/ZWSZC5z.gif" |
required |
out_gif |
str |
The output gif file. |
required |
colname |
str |
The column name of the vector that contains numerical values. |
required |
vmin |
float |
The minimum value to filter the data. Defaults to None. |
None |
vmax |
float |
The maximum value to filter the data. Defaults to None. |
None |
step |
float |
The step to filter the data. Defaults to 1. |
1 |
facecolor |
str |
The color to visualize the data. Defaults to "black". |
'black' |
figsize |
tuple |
The figure size. Defaults to (10, 8). |
(10, 8) |
padding |
int |
The padding of the figure tight_layout. Defaults to 3. |
3 |
title |
str |
The title of the figure. Defaults to None. |
None |
add_text |
bool |
Whether to add text to the figure. Defaults to True. |
True |
xy |
tuple |
The position of the text from the lower-left corner. Defaults to ("1%", "1%"). |
('1%', '1%') |
fontsize |
int |
The font size of the text. Defaults to 20. |
20 |
add_progress_bar |
bool |
Whether to add a progress bar to the figure. Defaults to True. |
True |
progress_bar_color |
str |
The color of the progress bar. Defaults to "blue". |
'blue' |
progress_bar_height |
int |
The height of the progress bar. Defaults to 5. |
5 |
dpi |
int |
The dpi of the figure. Defaults to 300. |
300 |
fps |
int |
The frames per second (fps) of the gif. Defaults to 10. |
10 |
loop |
int |
The number of loops of the gif. Defaults to 0, infinite loop. |
0 |
mp4 |
bool |
Whether to convert the gif to mp4. Defaults to False. |
False |
keep_png |
bool |
Whether to keep the png files. Defaults to False. |
False |
verbose |
bool |
Whether to print the progress. Defaults to True. |
True |
open_args |
dict |
The arguments for the geopandas.read_file() function. Defaults to {}. |
{} |
plot_args |
dict |
The arguments for the geopandas.GeoDataFrame.plot() function. Defaults to {}. |
{} |
Source code in leafmap/common.py
def vector_to_gif(
filename,
out_gif,
colname,
vmin=None,
vmax=None,
step=1,
facecolor="black",
figsize=(10, 8),
padding=3,
title=None,
add_text=True,
xy=("1%", "1%"),
fontsize=20,
add_progress_bar=True,
progress_bar_color="blue",
progress_bar_height=5,
dpi=300,
fps=10,
loop=0,
mp4=False,
keep_png=False,
verbose=True,
open_args={},
plot_args={},
):
"""Convert a vector to a gif. This function was inspired by by Johannes Uhl's shapefile2gif repo at
https://github.com/johannesuhl/shapefile2gif. Credits to Johannes Uhl.
Args:
filename (str): The input vector file. Can be a directory path or http URL, e.g., "https://i.imgur.com/ZWSZC5z.gif"
out_gif (str): The output gif file.
colname (str): The column name of the vector that contains numerical values.
vmin (float, optional): The minimum value to filter the data. Defaults to None.
vmax (float, optional): The maximum value to filter the data. Defaults to None.
step (float, optional): The step to filter the data. Defaults to 1.
facecolor (str, optional): The color to visualize the data. Defaults to "black".
figsize (tuple, optional): The figure size. Defaults to (10, 8).
padding (int, optional): The padding of the figure tight_layout. Defaults to 3.
title (str, optional): The title of the figure. Defaults to None.
add_text (bool, optional): Whether to add text to the figure. Defaults to True.
xy (tuple, optional): The position of the text from the lower-left corner. Defaults to ("1%", "1%").
fontsize (int, optional): The font size of the text. Defaults to 20.
add_progress_bar (bool, optional): Whether to add a progress bar to the figure. Defaults to True.
progress_bar_color (str, optional): The color of the progress bar. Defaults to "blue".
progress_bar_height (int, optional): The height of the progress bar. Defaults to 5.
dpi (int, optional): The dpi of the figure. Defaults to 300.
fps (int, optional): The frames per second (fps) of the gif. Defaults to 10.
loop (int, optional): The number of loops of the gif. Defaults to 0, infinite loop.
mp4 (bool, optional): Whether to convert the gif to mp4. Defaults to False.
keep_png (bool, optional): Whether to keep the png files. Defaults to False.
verbose (bool, optional): Whether to print the progress. Defaults to True.
open_args (dict, optional): The arguments for the geopandas.read_file() function. Defaults to {}.
plot_args (dict, optional): The arguments for the geopandas.GeoDataFrame.plot() function. Defaults to {}.
"""
import geopandas as gpd
import matplotlib.pyplot as plt
out_dir = os.path.dirname(out_gif)
tmp_dir = os.path.join(out_dir, "tmp_png")
if not os.path.exists(tmp_dir):
os.makedirs(tmp_dir)
if isinstance(filename, str):
gdf = gpd.read_file(filename, **open_args)
elif isinstance(filename, gpd.GeoDataFrame):
gdf = filename
else:
raise ValueError(
"filename must be a string or a geopandas.GeoDataFrame object."
)
bbox = gdf.total_bounds
if colname not in gdf.columns:
raise Exception(
f"{colname} is not in the columns of the GeoDataFrame. It must be one of {gdf.columns}"
)
values = gdf[colname].unique().tolist()
values.sort()
if vmin is None:
vmin = values[0]
if vmax is None:
vmax = values[-1]
options = range(vmin, vmax + step, step)
W = bbox[2] - bbox[0]
H = bbox[3] - bbox[1]
if xy is None:
# default text location is 5% width and 5% height of the image.
xy = (int(0.05 * W), int(0.05 * H))
elif (xy is not None) and (not isinstance(xy, tuple)) and (len(xy) == 2):
raise Exception("xy must be a tuple, e.g., (10, 10), ('10%', '10%')")
elif all(isinstance(item, int) for item in xy) and (len(xy) == 2):
x, y = xy
if (x > 0) and (x < W) and (y > 0) and (y < H):
pass
else:
print(
f"xy is out of bounds. x must be within [0, {W}], and y must be within [0, {H}]"
)
return
elif all(isinstance(item, str) for item in xy) and (len(xy) == 2):
x, y = xy
if ("%" in x) and ("%" in y):
try:
x = float(x.replace("%", "")) / 100.0 * W
y = float(y.replace("%", "")) / 100.0 * H
except Exception:
raise Exception(
"The specified xy is invalid. It must be formatted like this ('10%', '10%')"
)
else:
raise Exception(
"The specified xy is invalid. It must be formatted like this: (10, 10) or ('10%', '10%')"
)
x = bbox[0] + x
y = bbox[1] + y
for index, v in enumerate(options):
if verbose:
print(f"Processing {index+1}/{len(options)}: {v}...")
yrdf = gdf[gdf[colname] <= v]
fig, ax = plt.subplots()
ax = yrdf.plot(facecolor=facecolor, figsize=figsize, **plot_args)
ax.set_title(title, fontsize=fontsize)
ax.set_axis_off()
ax.set_xlim([bbox[0], bbox[2]])
ax.set_ylim([bbox[1], bbox[3]])
if add_text:
ax.text(x, y, v, fontsize=fontsize)
fig = ax.get_figure()
plt.tight_layout(pad=padding)
fig.savefig(tmp_dir + os.sep + "%s.png" % v, dpi=dpi)
plt.clf()
plt.close("all")
png_to_gif(tmp_dir, out_gif, fps=fps, loop=loop)
if add_progress_bar:
add_progress_bar_to_gif(
out_gif,
out_gif,
progress_bar_color,
progress_bar_height,
duration=1000 / fps,
loop=loop,
)
if mp4:
gif_to_mp4(out_gif, out_gif.replace(".gif", ".mp4"))
if not keep_png:
shutil.rmtree(tmp_dir)
if verbose:
print(f"Done. The GIF is saved to {out_gif}.")
vector_to_mbtiles(source_path, target_path, max_zoom=5, name=None, **kwargs)
¶
Convert a vector dataset to MBTiles format using the ogr2ogr command-line tool.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source_path |
str |
The path to the source vector dataset (GeoPackage, Shapefile, etc.). |
required |
target_path |
str |
The path to the target MBTiles file to be created. |
required |
max_zoom |
int |
The maximum zoom level for the MBTiles dataset. Defaults to 5. |
5 |
name |
str |
The name of the MBTiles dataset. Defaults to None. |
None |
**kwargs |
Additional options to be passed as keyword arguments. These options will be used as -dsco options when calling ogr2ogr. See https://gdal.org/drivers/raster/mbtiles.html for a list of options. |
{} |
Returns:
Type | Description |
---|---|
None |
None |
Exceptions:
Type | Description |
---|---|
subprocess.CalledProcessError |
If the ogr2ogr command fails to execute. |
Examples:
source_path = "countries.gpkg" target_path = "target.mbtiles" name = "My MBTiles" max_zoom = 5 vector_to_mbtiles(source_path, target_path, name=name, max_zoom=max_zoom)
Source code in leafmap/common.py
def vector_to_mbtiles(
source_path: str, target_path: str, max_zoom: int = 5, name: str = None, **kwargs
) -> None:
"""
Convert a vector dataset to MBTiles format using the ogr2ogr command-line tool.
Args:
source_path (str): The path to the source vector dataset (GeoPackage, Shapefile, etc.).
target_path (str): The path to the target MBTiles file to be created.
max_zoom (int, optional): The maximum zoom level for the MBTiles dataset. Defaults to 5.
name (str, optional): The name of the MBTiles dataset. Defaults to None.
**kwargs: Additional options to be passed as keyword arguments. These options will be used as -dsco options
when calling ogr2ogr. See https://gdal.org/drivers/raster/mbtiles.html for a list of options.
Returns:
None
Raises:
subprocess.CalledProcessError: If the ogr2ogr command fails to execute.
Example:
source_path = "countries.gpkg"
target_path = "target.mbtiles"
name = "My MBTiles"
max_zoom = 5
vector_to_mbtiles(source_path, target_path, name=name, max_zoom=max_zoom)
"""
import subprocess
command = [
"ogr2ogr",
"-f",
"MBTILES",
target_path,
source_path,
"-dsco",
f"MAXZOOM={max_zoom}",
]
if name:
command.extend(["-dsco", f"NAME={name}"])
for key, value in kwargs.items():
command.extend(["-dsco", f"{key.upper()}={value}"])
try:
subprocess.run(command, check=True)
except subprocess.CalledProcessError as e:
raise e
vector_to_parquet(source, output, crs=None, overwrite=False, **kwargs)
¶
Convert a GeoDataFrame or a file containing vector data to Parquet format.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source |
Union[gpd.GeoDataFrame, str] |
The source data to convert. It can be either a GeoDataFrame or a file path to the vector data file. |
required |
output |
str |
The file path where the Parquet file will be saved. |
required |
crs |
str |
The coordinate reference system (CRS) to use for the output file. Defaults to None. |
None |
overwrite |
bool |
Whether to overwrite the existing output file. Default is False. |
False |
**kwargs |
Additional keyword arguments to be passed to the |
{} |
Returns:
Type | Description |
---|---|
None |
None |
Source code in leafmap/common.py
def vector_to_parquet(
source: str, output: str, crs=None, overwrite=False, **kwargs
) -> None:
"""
Convert a GeoDataFrame or a file containing vector data to Parquet format.
Args:
source (Union[gpd.GeoDataFrame, str]): The source data to convert. It can be either a GeoDataFrame
or a file path to the vector data file.
output (str): The file path where the Parquet file will be saved.
crs (str, optional): The coordinate reference system (CRS) to use for the output file. Defaults to None.
overwrite (bool): Whether to overwrite the existing output file. Default is False.
**kwargs: Additional keyword arguments to be passed to the `to_parquet` function of GeoDataFrame.
Returns:
None
"""
import geopandas as gpd
if os.path.exists(output) and not overwrite:
print(f"File {output} already exists. Skipping...")
return
if isinstance(source, gpd.GeoDataFrame):
gdf = source
else:
gdf = gpd.read_file(source)
if crs is not None:
gdf = gdf.to_crs(crs)
out_dir = os.path.dirname(os.path.abspath(output))
if not os.path.exists(out_dir):
os.makedirs(out_dir)
gdf.to_parquet(output, **kwargs)
vector_to_pmtiles(source_path, target_path, max_zoom=5, name=None, **kwargs)
¶
Converts a vector file to PMTiles format.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source_path |
str |
Path to the source vector file. |
required |
target_path |
str |
Path to the target PMTiles file. |
required |
max_zoom |
int |
Maximum zoom level for the PMTiles. Defaults to 5. |
5 |
name |
str |
Name of the PMTiles dataset. Defaults to None. |
None |
**kwargs |
Additional keyword arguments to be passed to the underlying conversion functions. |
{} |
Exceptions:
Type | Description |
---|---|
ValueError |
If the target file does not have a .pmtiles extension. |
Returns:
Type | Description |
---|---|
None |
None |
Source code in leafmap/common.py
def vector_to_pmtiles(
source_path: str, target_path: str, max_zoom: int = 5, name: str = None, **kwargs
) -> None:
"""
Converts a vector file to PMTiles format.
Args:
source_path (str): Path to the source vector file.
target_path (str): Path to the target PMTiles file.
max_zoom (int, optional): Maximum zoom level for the PMTiles. Defaults to 5.
name (str, optional): Name of the PMTiles dataset. Defaults to None.
**kwargs: Additional keyword arguments to be passed to the underlying conversion functions.
Raises:
ValueError: If the target file does not have a .pmtiles extension.
Returns:
None
"""
if not target_path.endswith(".pmtiles"):
raise ValueError("Error: target file must be a .pmtiles file.")
mbtiles = target_path.replace(".pmtiles", ".mbtiles")
vector_to_mbtiles(source_path, mbtiles, max_zoom=max_zoom, name=name, **kwargs)
mbtiles_to_pmtiles(mbtiles, target_path)
os.remove(mbtiles)
vector_to_raster(vector, output, field='FID', assign='last', nodata=True, cell_size=None, base=None, callback=None, verbose=False, to_epsg=None)
¶
Convert a vector to a raster.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
vector |
str | GeoPandas.GeoDataFrame |
The input vector data, can be a file path or a GeoDataFrame. |
required |
output |
str |
The output raster file path. |
required |
field |
str |
Input field name in attribute table. Defaults to 'FID'. |
'FID' |
assign |
str |
Assignment operation, where multiple points are in the same grid cell; options include 'first', 'last' (default), 'min', 'max', 'sum', 'number'. Defaults to 'last'. |
'last' |
nodata |
bool |
Background value to set to NoData. Without this flag, it will be set to 0.0. |
True |
cell_size |
float |
Optionally specified cell size of output raster. Not used when base raster is specified |
None |
base |
str |
Optionally specified input base raster file. Not used when a cell size is specified. Defaults to None. |
None |
callback |
fuct |
A callback function to report progress. Defaults to None. |
None |
verbose |
bool |
Whether to print progress to the console. Defaults to False. |
False |
to_epsg |
integer |
Optionally specified the EPSG code to reproject the raster to. Defaults to None. |
None |
Source code in leafmap/common.py
def vector_to_raster(
vector,
output,
field="FID",
assign="last",
nodata=True,
cell_size=None,
base=None,
callback=None,
verbose=False,
to_epsg=None,
):
"""Convert a vector to a raster.
Args:
vector (str | GeoPandas.GeoDataFrame): The input vector data, can be a file path or a GeoDataFrame.
output (str): The output raster file path.
field (str, optional): Input field name in attribute table. Defaults to 'FID'.
assign (str, optional): Assignment operation, where multiple points are in the same grid cell; options
include 'first', 'last' (default), 'min', 'max', 'sum', 'number'. Defaults to 'last'.
nodata (bool, optional): Background value to set to NoData. Without this flag, it will be set to 0.0.
cell_size (float, optional): Optionally specified cell size of output raster. Not used when base raster is specified
base (str, optional): Optionally specified input base raster file. Not used when a cell size is specified. Defaults to None.
callback (fuct, optional): A callback function to report progress. Defaults to None.
verbose (bool, optional): Whether to print progress to the console. Defaults to False.
to_epsg (integer, optional): Optionally specified the EPSG code to reproject the raster to. Defaults to None.
"""
import geopandas as gpd
import whitebox
output = os.path.abspath(output)
if isinstance(vector, str):
gdf = gpd.read_file(vector)
elif isinstance(vector, gpd.GeoDataFrame):
gdf = vector
else:
raise TypeError("vector must be a file path or a GeoDataFrame")
if to_epsg is None:
to_epsg = 3857
if to_epsg == 4326:
raise ValueError("to_epsg cannot be 4326")
if gdf.crs.is_geographic:
gdf = gdf.to_crs(epsg=to_epsg)
vector = temp_file_path(extension=".shp")
gdf.to_file(vector)
else:
to_epsg = gdf.crs.to_epsg()
wbt = whitebox.WhiteboxTools()
wbt.verbose = verbose
goem_type = gdf.geom_type[0]
if goem_type == "LineString":
wbt.vector_lines_to_raster(
vector, output, field, nodata, cell_size, base, callback
)
elif goem_type == "Polygon":
wbt.vector_polygons_to_raster(
vector, output, field, nodata, cell_size, base, callback
)
else:
wbt.vector_points_to_raster(
vector, output, field, assign, nodata, cell_size, base, callback
)
image_set_crs(output, to_epsg)
view_lidar(filename, cmap='terrain', backend='pyvista', background=None, eye_dome_lighting=False, **kwargs)
¶
View LiDAR data in 3D.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filename |
str |
The filepath to the LiDAR data. |
required |
cmap |
str |
The colormap to use. Defaults to "terrain". cmap currently does not work for the open3d backend. |
'terrain' |
backend |
str |
The plotting backend to use, can be pyvista, ipygany, panel, and open3d. Defaults to "pyvista". |
'pyvista' |
background |
str |
The background color to use. Defaults to None. |
None |
eye_dome_lighting |
bool |
Whether to use eye dome lighting. Defaults to False. |
False |
Exceptions:
Type | Description |
---|---|
FileNotFoundError |
If the file does not exist. |
ValueError |
If the backend is not supported. |
Source code in leafmap/common.py
def view_lidar(
filename,
cmap="terrain",
backend="pyvista",
background=None,
eye_dome_lighting=False,
**kwargs,
):
"""View LiDAR data in 3D.
Args:
filename (str): The filepath to the LiDAR data.
cmap (str, optional): The colormap to use. Defaults to "terrain". cmap currently does not work for the open3d backend.
backend (str, optional): The plotting backend to use, can be pyvista, ipygany, panel, and open3d. Defaults to "pyvista".
background (str, optional): The background color to use. Defaults to None.
eye_dome_lighting (bool, optional): Whether to use eye dome lighting. Defaults to False.
Raises:
FileNotFoundError: If the file does not exist.
ValueError: If the backend is not supported.
"""
import sys
if os.environ.get("USE_MKDOCS") is not None:
return
if "google.colab" in sys.modules:
print("This function is not supported in Google Colab.")
return
warnings.filterwarnings("ignore")
filename = os.path.abspath(filename)
if not os.path.exists(filename):
raise FileNotFoundError(f"{filename} does not exist.")
backend = backend.lower()
if backend in ["pyvista", "ipygany", "panel"]:
try:
import pyntcloud
except ImportError:
print(
"The pyvista and pyntcloud packages are required for this function. Use pip install leafmap[lidar] to install them."
)
return
try:
if backend == "pyvista":
backend = None
if backend == "ipygany":
cmap = None
data = pyntcloud.PyntCloud.from_file(filename)
mesh = data.to_instance("pyvista", mesh=False)
mesh = mesh.elevation()
mesh.plot(
scalars="Elevation",
cmap=cmap,
jupyter_backend=backend,
background=background,
eye_dome_lighting=eye_dome_lighting,
**kwargs,
)
except Exception as e:
print("Something went wrong.")
print(e)
return
elif backend == "open3d":
try:
import laspy
import open3d as o3d
import numpy as np
except ImportError:
print(
"The laspy and open3d packages are required for this function. Use pip install laspy open3d to install them."
)
return
try:
las = laspy.read(filename)
point_data = np.stack([las.X, las.Y, las.Z], axis=0).transpose((1, 0))
geom = o3d.geometry.PointCloud()
geom.points = o3d.utility.Vector3dVector(point_data)
# geom.colors = o3d.utility.Vector3dVector(colors) # need to add colors. A list in the form of [[r,g,b], [r,g,b]] with value range 0-1. https://github.com/isl-org/Open3D/issues/614
o3d.visualization.draw_geometries([geom], **kwargs)
except Exception as e:
print("Something went wrong.")
print(e)
return
else:
raise ValueError(f"{backend} is not a valid backend.")
whiteboxgui(verbose=True, tree=False, reset=False, sandbox_path=None)
¶
Shows the WhiteboxTools GUI.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
verbose |
bool |
Whether to show progress info when the tool is running. Defaults to True. |
True |
tree |
bool |
Whether to use the tree mode toolbox built using ipytree rather than ipywidgets. Defaults to False. |
False |
reset |
bool |
Whether to regenerate the json file with the dictionary containing the information for all tools. Defaults to False. |
False |
sandbox_path |
str |
The path to the sandbox folder. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
object |
A toolbox GUI. |
Source code in leafmap/common.py
def whiteboxgui(
verbose: Optional[bool] = True,
tree: Optional[bool] = False,
reset: Optional[bool] = False,
sandbox_path: Optional[str] = None,
) -> dict:
"""Shows the WhiteboxTools GUI.
Args:
verbose (bool, optional): Whether to show progress info when the tool is running. Defaults to True.
tree (bool, optional): Whether to use the tree mode toolbox built using ipytree rather than ipywidgets. Defaults to False.
reset (bool, optional): Whether to regenerate the json file with the dictionary containing the information for all tools. Defaults to False.
sandbox_path (str, optional): The path to the sandbox folder. Defaults to None.
Returns:
object: A toolbox GUI.
"""
import whiteboxgui
return whiteboxgui.show(verbose, tree, reset, sandbox_path)
widget_template(widget=None, opened=True, show_close_button=True, widget_icon='gear', close_button_icon='times', widget_args={}, close_button_args={}, display_widget=None, m=None, position='topright')
¶
Create a widget template.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
widget |
ipywidgets.Widget |
The widget to be displayed. Defaults to None. |
None |
opened |
bool |
Whether to open the toolbar. Defaults to True. |
True |
show_close_button |
bool |
Whether to show the close button. Defaults to True. |
True |
widget_icon |
str |
The icon name for the toolbar button. Defaults to 'gear'. |
'gear' |
close_button_icon |
str |
The icon name for the close button. Defaults to "times". |
'times' |
widget_args |
dict |
Additional arguments to pass to the toolbar button. Defaults to {}. |
{} |
close_button_args |
dict |
Additional arguments to pass to the close button. Defaults to {}. |
{} |
display_widget |
ipywidgets.Widget |
The widget to be displayed when the toolbar is clicked. |
None |
m |
geemap.Map |
The geemap.Map instance. Defaults to None. |
None |
position |
str |
The position of the toolbar. Defaults to "topright". |
'topright' |
Source code in leafmap/common.py
def widget_template(
widget=None,
opened=True,
show_close_button=True,
widget_icon="gear",
close_button_icon="times",
widget_args={},
close_button_args={},
display_widget=None,
m=None,
position="topright",
):
"""Create a widget template.
Args:
widget (ipywidgets.Widget, optional): The widget to be displayed. Defaults to None.
opened (bool, optional): Whether to open the toolbar. Defaults to True.
show_close_button (bool, optional): Whether to show the close button. Defaults to True.
widget_icon (str, optional): The icon name for the toolbar button. Defaults to 'gear'.
close_button_icon (str, optional): The icon name for the close button. Defaults to "times".
widget_args (dict, optional): Additional arguments to pass to the toolbar button. Defaults to {}.
close_button_args (dict, optional): Additional arguments to pass to the close button. Defaults to {}.
display_widget (ipywidgets.Widget, optional): The widget to be displayed when the toolbar is clicked.
m (geemap.Map, optional): The geemap.Map instance. Defaults to None.
position (str, optional): The position of the toolbar. Defaults to "topright".
"""
name = "_" + random_string() # a random attribute name
if "value" not in widget_args:
widget_args["value"] = False
if "tooltip" not in widget_args:
widget_args["tooltip"] = "Toolbar"
if "layout" not in widget_args:
widget_args["layout"] = widgets.Layout(
width="28px", height="28px", padding="0px 0px 0px 4px"
)
widget_args["icon"] = widget_icon
if "value" not in close_button_args:
close_button_args["value"] = False
if "tooltip" not in close_button_args:
close_button_args["tooltip"] = "Close the tool"
if "button_style" not in close_button_args:
close_button_args["button_style"] = "primary"
if "layout" not in close_button_args:
close_button_args["layout"] = widgets.Layout(
height="28px", width="28px", padding="0px 0px 0px 4px"
)
close_button_args["icon"] = close_button_icon
toolbar_button = widgets.ToggleButton(**widget_args)
close_button = widgets.ToggleButton(**close_button_args)
toolbar_widget = widgets.VBox()
toolbar_widget.children = [toolbar_button]
toolbar_header = widgets.HBox()
if show_close_button:
toolbar_header.children = [close_button, toolbar_button]
else:
toolbar_header.children = [toolbar_button]
toolbar_footer = widgets.VBox()
if widget is not None:
toolbar_footer.children = [
widget,
]
else:
toolbar_footer.children = []
def toolbar_btn_click(change):
if change["new"]:
close_button.value = False
toolbar_widget.children = [toolbar_header, toolbar_footer]
if display_widget is not None:
widget.outputs = ()
with widget:
display(display_widget)
else:
toolbar_widget.children = [toolbar_button]
toolbar_button.observe(toolbar_btn_click, "value")
def close_btn_click(change):
if change["new"]:
toolbar_button.value = False
if m is not None:
control = getattr(m, name)
if control is not None and control in m.controls:
m.remove_control(control)
delattr(m, name)
toolbar_widget.close()
close_button.observe(close_btn_click, "value")
toolbar_button.value = opened
if m is not None:
import ipyleaflet
toolbar_control = ipyleaflet.WidgetControl(
widget=toolbar_widget, position=position
)
if toolbar_control not in m.controls:
m.add_control(toolbar_control)
setattr(m, name, toolbar_control)
else:
return toolbar_widget
write_lidar(source, destination, do_compress=None, laz_backend=None)
¶
Writes to a stream or file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source |
str | laspy.lasdatas.base.LasBase |
The source data to be written. |
required |
destination |
str |
The destination filepath. |
required |
do_compress |
bool |
Flags to indicate if you want to compress the data. Defaults to None. |
None |
laz_backend |
str |
The laz backend to use. Defaults to None. |
None |
Source code in leafmap/common.py
def write_lidar(source, destination, do_compress=None, laz_backend=None):
"""Writes to a stream or file.
Args:
source (str | laspy.lasdatas.base.LasBase): The source data to be written.
destination (str): The destination filepath.
do_compress (bool, optional): Flags to indicate if you want to compress the data. Defaults to None.
laz_backend (str, optional): The laz backend to use. Defaults to None.
"""
try:
import laspy
except ImportError:
print(
"The laspy package is required for this function. Use `pip install laspy[lazrs,laszip]` to install it."
)
return
if isinstance(source, str):
source = read_lidar(source)
source.write(destination, do_compress=do_compress, laz_backend=laz_backend)
xarray_to_raster(dataset, filename, **kwargs)
¶
Convert an xarray Dataset to a raster file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
dataset |
xr.Dataset |
The input xarray Dataset to be converted. |
required |
filename |
str |
The output filename for the raster file. |
required |
**kwargs |
Dict[str, Any] |
Additional keyword arguments passed to the |
{} |
Returns:
Type | Description |
---|---|
None |
None |
Source code in leafmap/common.py
def xarray_to_raster(dataset, filename: str, **kwargs: Dict[str, Any]) -> None:
"""Convert an xarray Dataset to a raster file.
Args:
dataset (xr.Dataset): The input xarray Dataset to be converted.
filename (str): The output filename for the raster file.
**kwargs (Dict[str, Any]): Additional keyword arguments passed to the `rio.to_raster()` method.
See https://corteva.github.io/rioxarray/stable/examples/convert_to_raster.html for more info.
Returns:
None
"""
import rioxarray
dims = list(dataset.dims)
new_names = {}
if "lat" in dims:
new_names["lat"] = "y"
dims.remove("lat")
if "lon" in dims:
new_names["lon"] = "x"
dims.remove("lon")
if "lng" in dims:
new_names["lng"] = "x"
dims.remove("lng")
if "latitude" in dims:
new_names["latitude"] = "y"
dims.remove("latitude")
if "longitude" in dims:
new_names["longitude"] = "x"
dims.remove("longitude")
dataset = dataset.rename(new_names)
dataset.transpose(..., "y", "x").rio.to_raster(filename, **kwargs)
xy_to_window(xy)
¶
Converts a list of coordinates to a rasterio window.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
xy |
list |
A list of coordinates in the format of [[x1, y1], [x2, y2]] |
required |
Returns:
Type | Description |
---|---|
tuple |
The rasterio window in the format of (col_off, row_off, width, height) |
Source code in leafmap/common.py
def xy_to_window(xy):
"""Converts a list of coordinates to a rasterio window.
Args:
xy (list): A list of coordinates in the format of [[x1, y1], [x2, y2]]
Returns:
tuple: The rasterio window in the format of (col_off, row_off, width, height)
"""
x1, y1 = xy[0]
x2, y2 = xy[1]
left = min(x1, x2)
right = max(x1, x2)
top = min(y1, y2)
bottom = max(y1, y2)
width = right - left
height = bottom - top
return (left, top, width, height)
zonal_stats(vectors, raster, layer=0, band_num=1, nodata=None, affine=None, stats=None, all_touched=False, categorical=False, category_map=None, add_stats=None, raster_out=False, prefix=None, geojson_out=False, gdf_out=False, dst_crs=None, open_vector_args={}, open_raster_args={}, **kwargs)
¶
This function wraps rasterstats.zonal_stats and performs reprojection if necessary. See https://pythonhosted.org/rasterstats/rasterstats.html.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
vectors |
str | list | GeoDataFrame |
path to an vector source or geo-like python objects. |
required |
raster |
str | ndarray |
ndarray or path to a GDAL raster source. |
required |
layer |
int |
If vectors is a path to an fiona source, specify the vector layer to use either by name or number. Defaults to 0 |
0 |
band_num |
int | str |
If raster is a GDAL source, the band number to use (counting from 1). defaults to 1. |
1 |
nodata |
float |
If raster is a GDAL source, this value overrides any NODATA value specified in the file’s metadata. If None, the file’s metadata’s NODATA value (if any) will be used. defaults to None. |
None |
affine |
Affine |
required only for ndarrays, otherwise it is read from src. Defaults to None. |
None |
stats |
str | list |
Which statistics to calculate for each zone. It can be ['min', 'max', 'mean', 'count']. For more, see https://pythonhosted.org/rasterstats/manual.html#zonal-statistics Defaults to None. |
None |
all_touched |
bool |
Whether to include every raster cell touched by a geometry, or only those having a center point within the polygon. defaults to False |
False |
categorical |
bool |
If True, the raster values will be treated as categorical. |
False |
category_map |
dict |
A dictionary mapping raster values to human-readable categorical names. Only applies when categorical is True |
None |
add_stats |
dict |
with names and functions of additional stats to compute. Defaults to None. |
None |
raster_out |
bool |
Include the masked numpy array for each feature?. Defaults to False. |
False |
prefix |
str |
add a prefix to the keys. Defaults to None. |
None |
geojson_out |
bool |
Return list of GeoJSON-like features (default: False) Original feature geometry and properties will be retained with zonal stats appended as additional properties. Use with prefix to ensure unique and meaningful property names.. Defaults to False. |
False |
gdf_out |
bool |
Return a GeoDataFrame. Defaults to False. |
False |
dst_crs |
str |
The destination CRS. Defaults to None. |
None |
open_vector_args |
dict |
Pass additional arguments to geopandas.open_file(). Defaults to {}. |
{} |
open_raster_args |
dict |
Pass additional arguments to rasterio.open(). Defaults to {}. |
{} |
Returns:
Type | Description |
---|---|
dict | list | GeoDataFrame |
The zonal statistics results |
Source code in leafmap/common.py
def zonal_stats(
vectors,
raster,
layer=0,
band_num=1,
nodata=None,
affine=None,
stats=None,
all_touched=False,
categorical=False,
category_map=None,
add_stats=None,
raster_out=False,
prefix=None,
geojson_out=False,
gdf_out=False,
dst_crs=None,
open_vector_args={},
open_raster_args={},
**kwargs,
):
"""This function wraps rasterstats.zonal_stats and performs reprojection if necessary.
See https://pythonhosted.org/rasterstats/rasterstats.html.
Args:
vectors (str | list | GeoDataFrame): path to an vector source or geo-like python objects.
raster (str | ndarray): ndarray or path to a GDAL raster source.
layer (int, optional): If vectors is a path to an fiona source, specify the vector layer to
use either by name or number. Defaults to 0
band_num (int | str, optional): If raster is a GDAL source, the band number to use (counting from 1). defaults to 1.
nodata (float, optional): If raster is a GDAL source, this value overrides any NODATA value
specified in the file’s metadata. If None, the file’s metadata’s NODATA value (if any)
will be used. defaults to None.
affine (Affine, optional): required only for ndarrays, otherwise it is read from src. Defaults to None.
stats (str | list, optional): Which statistics to calculate for each zone.
It can be ['min', 'max', 'mean', 'count']. For more, see https://pythonhosted.org/rasterstats/manual.html#zonal-statistics
Defaults to None.
all_touched (bool, optional): Whether to include every raster cell touched by a geometry, or only those having
a center point within the polygon. defaults to False
categorical (bool, optional): If True, the raster values will be treated as categorical.
category_map (dict, optional):A dictionary mapping raster values to human-readable categorical names.
Only applies when categorical is True
add_stats (dict, optional): with names and functions of additional stats to compute. Defaults to None.
raster_out (bool, optional): Include the masked numpy array for each feature?. Defaults to False.
prefix (str, optional): add a prefix to the keys. Defaults to None.
geojson_out (bool, optional): Return list of GeoJSON-like features (default: False)
Original feature geometry and properties will be retained with zonal stats
appended as additional properties. Use with prefix to ensure unique and
meaningful property names.. Defaults to False.
gdf_out (bool, optional): Return a GeoDataFrame. Defaults to False.
dst_crs (str, optional): The destination CRS. Defaults to None.
open_vector_args (dict, optional): Pass additional arguments to geopandas.open_file(). Defaults to {}.
open_raster_args (dict, optional): Pass additional arguments to rasterio.open(). Defaults to {}.
Returns:
dict | list | GeoDataFrame: The zonal statistics results
"""
import geopandas as gpd
import rasterio
try:
import rasterstats
except ImportError:
raise ImportError(
"rasterstats is not installed. Install it with pip install rasterstats"
)
try:
if isinstance(raster, str):
with rasterio.open(raster, **open_raster_args) as src:
affine = src.transform
nodata = src.nodata
array = src.read(band_num, masked=True)
raster_crs = src.crs
elif isinstance(raster, rasterio.io.DatasetReader):
affine = raster.transform
nodata = raster.nodata
array = raster.read(band_num, masked=True)
raster_crs = raster.crs
else:
array = raster
if isinstance(vectors, str):
gdf = gpd.read_file(vectors, **open_vector_args)
elif isinstance(vectors, list):
gdf = gpd.GeoDataFrame.from_features(vectors)
else:
gdf = vectors
vector_crs = gdf.crs
if gdf.crs.is_geographic:
if not raster_crs.is_geographic:
gdf = gdf.to_crs(raster_crs)
elif gdf.crs != raster_crs:
if not raster_crs.is_geographic:
gdf = gdf.to_crs(raster_crs)
else:
raise ValueError("The vector and raster CRSs are not compatible")
if gdf_out is True:
geojson_out = True
result = rasterstats.zonal_stats(
gdf,
array,
layer=layer,
band_num=band_num,
nodata=nodata,
affine=affine,
stats=stats,
all_touched=all_touched,
categorical=categorical,
category_map=category_map,
add_stats=add_stats,
raster_out=raster_out,
prefix=prefix,
geojson_out=geojson_out,
**kwargs,
)
if gdf_out is True:
if dst_crs is None:
dst_crs = vector_crs
out_gdf = gpd.GeoDataFrame.from_features(result)
out_gdf.crs = raster_crs
return out_gdf.to_crs(dst_crs)
else:
return result
except Exception as e:
raise Exception(e)
zoom_level_resolution(zoom, latitude=0)
¶
Returns the approximate pixel scale based on zoom level and latutude. See https://blogs.bing.com/maps/2006/02/25/map-control-zoom-levels-gt-resolution
Parameters:
Name | Type | Description | Default |
---|---|---|---|
zoom |
int |
The zoom level. |
required |
latitude |
float |
The latitude. Defaults to 0. |
0 |
Returns:
Type | Description |
---|---|
float |
Map resolution in meters. |
Source code in leafmap/common.py
def zoom_level_resolution(zoom, latitude=0):
"""Returns the approximate pixel scale based on zoom level and latutude.
See https://blogs.bing.com/maps/2006/02/25/map-control-zoom-levels-gt-resolution
Args:
zoom (int): The zoom level.
latitude (float, optional): The latitude. Defaults to 0.
Returns:
float: Map resolution in meters.
"""
import math
resolution = 156543.04 * math.cos(latitude) / math.pow(2, zoom)
return abs(resolution)