From 0a5eb31c9134de0397945ce9e476770bedf0c922 Mon Sep 17 00:00:00 2001 From: Kurt Schwehr Date: Tue, 30 Dec 2025 14:28:49 -0800 Subject: [PATCH 1/4] common.py: Cleanup * Move import webbrowser and box to the top * Remove redundant imports of requests * Add more type annotations * Remove types from doc strings when type annotations are present * Fit to 88 columns * Convert doc strings to start with "Returns" and remove "Returns:" section * Remove commented out code * Remove redundant comments * Add missing puctuation to comment --- geemap/common.py | 412 +++++++++++++++++++++++------------------------ 1 file changed, 200 insertions(+), 212 deletions(-) diff --git a/geemap/common.py b/geemap/common.py index ac9dc6c560..50cf0e2ce0 100644 --- a/geemap/common.py +++ b/geemap/common.py @@ -37,8 +37,10 @@ import urllib.request from urllib.request import urlopen import warnings +import webbrowser import zipfile +import box import ee import ipyleaflet import ipywidgets as widgets @@ -61,53 +63,53 @@ def ee_export_image( - ee_object, - filename, - scale=None, - crs=None, - crs_transform=None, - region=None, - dimensions=None, - file_per_band=False, - format="ZIPPED_GEO_TIFF", - unzip=True, - unmask_value=None, - timeout=300, - proxies=None, - verbose=True, -): + ee_object: Any, + filename: str, + scale: float | None = None, + crs: str | None = None, + crs_transform: list[float] | None = None, + region: Any | None = None, + dimensions: list[int] | None = None, + file_per_band: bool = False, + format: str = "ZIPPED_GEO_TIFF", + unzip: bool = True, + unmask_value: float | None = None, + timeout: int = 300, + proxies: dict[str, Any] | None = None, + verbose: bool = True, +) -> None: """Exports an ee.Image as a GeoTIFF. Args: - ee_object (object): The ee.Image to download. - filename (str): Output filename for the exported image. - scale (float, optional): A default scale to use for any bands that do not - specify one; ignored if crs and crs_transform is specified. Defaults to - None. - crs (str, optional): A default CRS string to use for any bands that do not - explicitly specify one. Defaults to None. - crs_transform (list, optional): a default affine transform to use for any bands - that do not specify one, of the same format as the crs_transform of - bands. Defaults to None. - region (object, optional): A polygon specifying a region to download; ignored if + ee_object: The ee.Image to download. + filename: Output filename for the exported image. + scale: A default scale to use for any bands that do not specify one; ignored if crs and crs_transform is specified. Defaults to None. - dimensions (list, optional): An optional array of two integers defining the - width and height to which the band is cropped. Defaults to None. - file_per_band (bool, optional): Whether to produce a different GeoTIFF per - band. Defaults to False. + crs: A default CRS string to use for any bands that do not explicitly specify + one. Defaults to None. + crs_transform: a default affine transform to use for any bands that do not + specify one, of the same format as the crs_transform of bands. Defaults to + None. + region: A polygon specifying a region to download; ignored if crs and + crs_transform is specified. Defaults to None. + dimensions: An optional array of two integers defining the width and height to + which the band is cropped. Defaults to None. + file_per_band: Whether to produce a different GeoTIFF per band. Defaults to + False. format (str, optional): One of: "ZIPPED_GEO_TIFF" (GeoTIFF file(s) wrapped in a zip file, default), "GEO_TIFF" (GeoTIFF file), "NPY" (NumPy binary format). If "GEO_TIFF" or "NPY", filePerBand and all band-level transformations will be ignored. Loading a NumPy output results in a structured array. - unzip (bool, optional): Whether to unzip the downloaded file. Defaults to True. + unzip: Whether to unzip the downloaded file. Defaults to True. + unmask_value (float, optional): The value to use for pixels that are masked in the input image. If the exported image contains zero values, you should set the unmask value to a non-zero value so that the zero values are not treated as missing data. Defaults to None. - timeout (int, optional): The timeout in seconds for the request. Defaults to 300. - proxies (dict, optional): A dictionary of proxy servers to use. Defaults to None. - verbose (bool, optional): Whether to print out descriptive text. Defaults to True. + timeout: The timeout in seconds for the request. Defaults to 300. + proxies: A dictionary of proxy servers to use. Defaults to None. + verbose: Whether to print out descriptive text. Defaults to True. """ if not isinstance(ee_object, ee.Image): @@ -194,60 +196,57 @@ def ee_export_image( def ee_export_image_collection( - ee_object, - out_dir, - scale=None, - crs=None, - crs_transform=None, - region=None, - dimensions=None, - file_per_band=False, - format="ZIPPED_GEO_TIFF", - unmask_value=None, + ee_object: Any, + out_dir: str, + scale: float | None = None, + crs: str | None = None, + crs_transform: list[float] | None = None, + region: Any = None, + dimensions: list[float] = None, + file_per_band: bool = False, + format: str = "ZIPPED_GEO_TIFF", + unmask_value: float | None = None, filenames=None, - timeout=300, - proxies=None, - verbose=True, + timeout: int = 300, + proxies: dict[str, Any] = None, + verbose: bool = True, ): """Exports an ImageCollection as GeoTIFFs. Args: - ee_object (object): The ee.Image to download. - out_dir (str): The output directory for the exported images. - - scale (float, optional): A default scale to use for any bands that do not - specify one; ignored if crs and crs_transform is specified. Defaults to - None. - crs (str, optional): A default CRS string to use for any bands that do not - explicitly specify one. Defaults to None. - crs_transform (list, optional): a default affine transform to use for any bands - that do not specify one, of the same format as the crs_transform of - bands. Defaults to None. - region (object, optional): A polygon specifying a region to download; ignored if + ee_object: The ee.Image to download. + out_dir: The output directory for the exported images. + scale: A default scale to use for any bands that do not specify one; ignored if crs and crs_transform is specified. Defaults to None. - dimensions (list, optional): An optional array of two integers defining the - width and height to which the band is cropped. Defaults to None. - file_per_band (bool, optional): Whether to produce a different GeoTIFF per - band. Defaults to False. - format (str, optional): One of: "ZIPPED_GEO_TIFF" (GeoTIFF file(s) wrapped in a - zip file, default), "GEO_TIFF" (GeoTIFF file), "NPY" (NumPy binary - format). If "GEO_TIFF" or "NPY", filePerBand and all band-level - transformations will be ignored. Loading a NumPy output results in a - structured array. - unmask_value (float, optional): The value to use for pixels that are masked in - the input image. If the exported image contains zero values, you should set - the unmask value to a non-zero value so that the zero values are not treated - as missing data. Defaults to None. + crs: A default CRS string to use for any bands that do not explicitly specify + one. Defaults to None. + crs_transform: a default affine transform to use for any bands that do not + specify one, of the same format as the crs_transform of bands. Defaults to + None. + region: A polygon specifying a region to download; ignored if crs and + crs_transform is specified. Defaults to None. + dimensions: An optional array of two integers defining the width and height to + which the band is cropped. Defaults to None. + file_per_band: Whether to produce a different GeoTIFF per band. Defaults to + False. + format: One of: "ZIPPED_GEO_TIFF" (GeoTIFF file(s) wrapped in a zip file, + default), "GEO_TIFF" (GeoTIFF file), "NPY" (NumPy binary format). If + "GEO_TIFF" or "NPY", filePerBand and all band-level transformations will be + ignored. Loading a NumPy output results in a structured array. + unmask_value: The value to use for pixels that are masked in the input image. + If the exported image contains zero values, you should set the unmask value + to a non-zero value so that the zero values are not treated as missing + data. Defaults to None. filenames (list | int, optional): A list of filenames to use for the exported images. Defaults to None. - timeout (int, optional): The timeout in seconds for the request. + timeout: The timeout in seconds for the request. Defaults to 300. - proxies (dict, optional): A dictionary of proxy servers to use. + proxies: A dictionary of proxy servers to use. Defaults to None. - verbose (bool, optional): Whether to print out descriptive text. + verbose: Whether to print out descriptive text. Defaults to True. """ - + # TODO(schwehr): Fix the doc string and type annotation. if not isinstance(ee_object, ee.ImageCollection): print("The ee_object must be an ee.ImageCollection.") return @@ -361,7 +360,6 @@ def ee_export_image_to_drive( **kwargs: Holds other keyword arguments that may have been deprecated such as 'crs_transform', 'driveFolder', and 'driveFileNamePrefix'. """ - if not isinstance(image, ee.Image): raise ValueError("Input image must be an instance of ee.Image") @@ -865,18 +863,21 @@ def ee_export_image_collection_to_cloud_storage( def ee_export_geojson( - ee_object, filename=None, selectors=None, timeout=300, proxies=None -): + ee_object: Any, + filename: str | None = None, + selectors: list[str] | None = None, + timeout: int = 300, + proxies: dict[str, None] = None, +) -> None: """Exports Earth Engine FeatureCollection to geojson. Args: - ee_object (object): ee.FeatureCollection to export. - filename (str): Output file name. Defaults to None. - selectors (list, optional): A list of attributes to export. Defaults to None. - timeout (int, optional): Timeout in seconds. Defaults to 300 seconds. - proxies (dict, optional): Proxy settings. Defaults to None. + ee_object: ee.FeatureCollection to export. + filename: Output file name. Defaults to None. + selectors: A list of attributes to export. Defaults to None. + timeout: Timeout in seconds. Defaults to 300 seconds. + proxies: Proxy settings. Defaults to None. """ - if not isinstance(ee_object, ee.FeatureCollection): print("The ee_object must be an ee.FeatureCollection.") return @@ -952,28 +953,27 @@ def ee_export_geojson( def ee_export_vector( - ee_object, - filename, - selectors=None, - verbose=True, - keep_zip=False, - timeout=300, - proxies=None, + ee_object: Any, + filename: str, + selectors: list[str] | None = None, + verbose: bool = True, + keep_zip: bool = False, + timeout: int = 300, + proxies: dict[str, Any] = None, ): """Exports Earth Engine FeatureCollection to other formats. Includes shp, csv, json, kml, and kmz. Args: - ee_object (object): ee.FeatureCollection to export. - filename (str): Output file name. - selectors (list, optional): A list of attributes to export. Defaults to None. - verbose (bool, optional): Whether to print out descriptive text. - keep_zip (bool, optional): Whether to keep the shapefile as a zip file. - timeout (int, optional): Timeout in seconds. Defaults to 300 seconds. - proxies (dict, optional): A dictionary of proxies to use. Defaults to None. + ee_object: ee.FeatureCollection to export. + filename: Output file name. + selectors: A list of attributes to export. Defaults to None. + verbose: Whether to print out descriptive text. + keep_zip: Whether to keep the shapefile as a zip file. + timeout: Timeout in seconds. Defaults to 300 seconds. + proxies: A dictionary of proxies to use. Defaults to None. """ - if not isinstance(ee_object, ee.FeatureCollection): raise ValueError("ee_object must be an ee.FeatureCollection") @@ -1138,10 +1138,10 @@ def ee_export_vector_to_asset( collection: The feature collection to be exported. description: Human-readable name of the task. assetId: The destination asset ID. - maxVertices: - Max number of uncut vertices per geometry; geometries with more + maxVertices: Max number of uncut vertices per geometry; geometries with more vertices will be cut into pieces smaller than this size. **kwargs: Holds other keyword arguments that may have been deprecated. + """ if not isinstance(collection, ee.FeatureCollection): raise ValueError("The collection must be an ee.FeatureCollection.") @@ -1539,7 +1539,6 @@ def __init__( """Initialize the TitilerEndpoint object. Args: - endpoint: The endpoint of the titiler server. Defaults to "https://giswqs-titiler-endpoint.hf.space". name: The name to be used in the file path. Defaults to "stac". @@ -1689,11 +1688,7 @@ def set_proxy( def is_drive_mounted() -> bool: - """Checks whether Google Drive is mounted in Google Colab. - - Returns: - bool: Returns True if Google Drive is mounted, False otherwise. - """ + """Returns True if Google Drive is mounted, False otherwise.""" drive_path = "/content/drive/My Drive" if os.path.exists(drive_path): return True @@ -1702,11 +1697,7 @@ def is_drive_mounted() -> bool: def credentials_in_drive() -> bool: - """Checks if the ee credentials file exists in Google Drive. - - Returns: - bool: Returns True if Google Drive is mounted, False otherwise. - """ + """Returns True if Google Drive is mounted, False otherwise.""" credentials_path = "/content/drive/My Drive/.config/earthengine/credentials" if os.path.exists(credentials_path): return True @@ -1762,11 +1753,10 @@ def check_install(package: str) -> None: If not, it will install the package. Args: - package (str): The name of the package to check. + package: The name of the package to check. """ try: __import__(package) - # print('{} is already installed.'.format(package)) except ImportError: print(f"{package} is not installed. Installing ...") try: @@ -1782,8 +1772,8 @@ def update_package(): Avoids 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. + In this way, I don't have to keep updating pypi and conda-forge with every minor + update of the package. """ download_dir = os.path.join(os.path.expanduser("~"), "Downloads") if not os.path.exists(download_dir): @@ -1822,8 +1812,8 @@ def install_package(package: str | list[str]) -> None: """Install a Python package. Args: - package: The package name or a GitHub URL or a list of package - names or GitHub URLs. + package: The package name or a GitHub URL or a list of package names or GitHub + URLs. """ if isinstance(package, str): packages = [package] @@ -1898,8 +1888,6 @@ def check_git_install() -> bool: Returns: bool: Returns True if Git is installed, otherwise returns False. """ - import webbrowser - cmd = "git --version" output = os.popen(cmd).read() @@ -1982,8 +1970,6 @@ def open_github(subdir: str | None = None) -> None: Args: subdir: Sub-directory of the repository. Defaults to None. """ - import webbrowser - url = "https://github.com/gee-community/geemap" if subdir == "source": @@ -1998,8 +1984,6 @@ def open_github(subdir: str | None = None) -> None: def open_youtube() -> None: """Opens the YouTube tutorials for geemap.""" - import webbrowser - url = "https://www.youtube.com/playlist?list=PLAxJ4-o7ZoPccOFv1dCwvGI6TYnirRTg3" webbrowser.open_new_tab(url) @@ -2015,13 +1999,13 @@ def is_tool(name: str) -> bool: return shutil.which(name) is not None -def open_image_from_url(url, timeout=300, proxies=None): +def open_image_from_url(url: str, timeout: int = 300, proxies: dict[str, str] = None): """Loads an image from the specified URL. Args: - url (str): URL of the image. - timeout (int, optional): Timeout in seconds. Defaults to 300. - proxies (dict, optional): Dictionary of proxies. Defaults to None. + url: URL of the image. + timeout: Timeout in seconds. Defaults to 300. + proxies: Dictionary of proxies. Defaults to None. Returns: object: Image object. @@ -2037,13 +2021,13 @@ def open_image_from_url(url, timeout=300, proxies=None): print(e) -def show_image(img_path, width=None, height=None): +def show_image(img_path: str, width: int | None = None, height: int | None = 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. + img_path: The image file path. + width: Width of the image in pixels. Defaults to None. + height: Height of the image in pixels. Defaults to None. """ from IPython.display import display @@ -2069,17 +2053,17 @@ def show_image(img_path, width=None, height=None): print(e) -def show_html(html): +def show_html(html: str) -> widgets.HTML: """Shows HTML within Jupyter notebook. Args: - html (str): File path or HTML string. + html: File path or HTML string. Raises: FileNotFoundError: If the file does not exist. Returns: - ipywidgets.HTML: HTML widget. + HTML widget. """ if os.path.exists(html): with open(html) as f: @@ -2095,16 +2079,15 @@ def show_html(html): raise Exception(e) -def has_transparency(img): +def has_transparency(img) -> bool: """Checks whether an image has transparency. Args: - img (object): a PIL Image object. + img: 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(): @@ -2118,11 +2101,11 @@ def has_transparency(img): return False -def upload_to_imgur(in_gif): +def upload_to_imgur(in_gif: str): """Uploads an image to imgur.com Args: - in_gif (str): The file path to the image. + in_gif: The file path to the image. """ pkg_name = "imgur-uploader" if not is_tool(pkg_name): @@ -2142,14 +2125,11 @@ def upload_to_imgur(in_gif): 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" + "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 @@ -2162,8 +2142,8 @@ def upload_to_imgur(in_gif): ######################################## -def system_fonts(show_full_path=False): - """Gets a list of system fonts +def system_fonts(show_full_path: bool = False) -> list[str]: + """Returns a list of system fonts. # Common font locations: # Linux: /usr/share/fonts/TTF/ @@ -2171,10 +2151,8 @@ def system_fonts(show_full_path=False): # 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. + show_full_path: Whether to show the full path of each system font. Defaults to + False. """ try: font_list = matplotlib.font_manager.findSystemFonts( @@ -2199,15 +2177,26 @@ def system_fonts(show_full_path=False): ######################################## -def 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/giswqs/whitebox/raw/master/examples/testdata.zip) +def download_from_url( + url: str, + out_file_name: str | None = None, + out_dir: str = ".", + unzip: bool = True, + verbose: bool = True, +) -> None: + """Download a file from a URL. + + For example: + + https://github.com/giswqs/whitebox/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 + url: The HTTP URL to download. + out_file_name: The output file name to use. Defaults to None. + out_dir: The output directory to use. Defaults to '.'. + unzip: Whether to unzip the downloaded file if it is a zip file. Defaults to + True. + verbose: Whether to display or not the output of the function. """ in_file_name = os.path.basename(url) @@ -2226,7 +2215,6 @@ def download_from_url(url, out_file_name=None, out_dir=".", unzip=True, verbose= final_path = out_file_path if unzip: - # if it is a zip file if ".zip" in out_file_name: if verbose: print(f"Unzipping {out_file_name} ...") @@ -2236,7 +2224,6 @@ def download_from_url(url, out_file_name=None, out_dir=".", unzip=True, verbose= 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(f"Unzipping {out_file_name} ...") @@ -2270,22 +2257,32 @@ def safe_extract( print(f"Data downloaded to: {final_path}") -def download_from_gdrive(gfile_url, file_name, out_dir=".", unzip=True, verbose=True): - """Download a file shared via Google Drive - (e.g., https://drive.google.com/file/d/18SUo_HcDGltuWYZs1s7PpOmOq_FvFn04/view?usp=sharing) +def download_from_gdrive( + gfile_url: str, + file_name: str, + out_dir: str = ".", + unzip: bool = True, + verbose: bool = True, +) -> None: + """Download a file shared via Google Drive. + + For example: + + https://drive.google.com/file/d/18SUo_HcDGltuWYZs1s7PpOmOq_FvFn04/view?usp=sharing Args: - gfile_url (str): The Google Drive shared file URL - file_name (str): The output file name to use. - out_dir (str, optional): The output directory. Defaults to '.'. - unzip (bool, optional): Whether to unzip the output file if it is a zip file. Defaults to True. - verbose (bool, optional): Whether to display or not the output of the function + gfile_url: The Google Drive shared file URL + file_name: The output file name to use. + out_dir: The output directory. Defaults to '.'. + unzip: Whether to unzip the output file if it is a zip file. Defaults to True. + verbose: Whether to display or not the output of the function """ try: from google_drive_downloader import GoogleDriveDownloader as gdd except ImportError: raise Exception( - "Please install the google_drive_downloader package using `pip install googledrivedownloader`" + "Please install the google_drive_downloader package using " + "`pip install googledrivedownloader`." ) file_id = gfile_url.split("/")[5] @@ -2296,12 +2293,14 @@ def download_from_gdrive(gfile_url, file_name, out_dir=".", unzip=True, verbose= gdd.coreutils.download_file_from_google_drive(file_id, dest_path, True, unzip) -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 +def create_download_link(filename: str, title: str = "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: ". + filename: The file path to the file to download + title: Defaults to "Click here to download: ". Returns: str: HTML download URL. @@ -2317,18 +2316,26 @@ def create_download_link(filename, title="Click here to download: "): return HTML(html) -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 +def edit_download_html( + htmlWidget, filename: str, title: str = "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: ". + filename: File path to download. + title: Download description. Defaults to "Click here to download: ". """ - # Change widget html temporarily to a font-awesome spinner - htmlWidget.value = 'Loading...' + # Change widget html temporarily to a font-awesome spinner. + htmlWidget.value = ( + '' + 'Loading...' + ) - # Process raw data + # Process raw data. data = open(filename, "rb").read() b64 = base64.b64encode(data) payload = b64.decode() @@ -2341,9 +2348,6 @@ def edit_download_html(htmlWidget, filename, title="Click here to download: "): payload=payload, title=title + basename, filename=basename ) - # htmlWidget = widgets.HTML(value = '') - # htmlWidget - ######################################## # Data Conversion # @@ -3482,8 +3486,6 @@ def screen_capture(filename, monitor=1): def api_docs(): """Open a browser and navigate to the geemap API documentation.""" - import webbrowser - url = "https://geemap.org/geemap" webbrowser.open_new_tab(url) @@ -8642,8 +8644,6 @@ def vector_styling( Returns: object: An ee.FeatureCollection containing the styling attribute. """ - import box - if isinstance(ee_object, ee.FeatureCollection): prop_names = ee.Feature(ee_object.first()).propertyNames().getInfo() arr = ee_object.aggregate_array(column).distinct().sort() @@ -10240,15 +10240,13 @@ def search_qms(keyword, limit=10, list_only=True, add_prefix=True, timeout=300): return None -def get_wms_layers(url, return_titles=False): +def get_wms_layers(url: str, return_titles: bool = False): """Returns a list of WMS layers from a WMS service. Args: - url (str): The URL of the WMS service. - return_titles (bool, optional): If True, the titles of the layers will be returned. Defaults to False. - - Returns: - list: A list of WMS layers. + url: The URL of the WMS service. + return_titles: If True, the titles of the layers will be returned. Defaults to + False. """ from owslib.wms import WebMapService @@ -10261,19 +10259,18 @@ def get_wms_layers(url, return_titles=False): return layers -def read_file_from_url(url, return_type="list", encoding="utf-8"): - """Reads a file from a URL. +def read_file_from_url( + url: str, return_type: str = "list", encoding: str = "utf-8" +) -> list[str] | str: + """Returns the contents of 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". + url: The URL of the file. + return_type: The return type, can either be string or list. Defaults to "list". + encoding: 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. """ if return_type == "list": return [line.decode(encoding).rstrip() for line in urlopen(url).readlines()] @@ -11675,8 +11672,6 @@ def blend( Returns: ee.Image: The blended image. """ - import box - if not isinstance(top_layer, ee.Image): raise ValueError("top_layer must be an ee.Image.") @@ -14653,8 +14648,6 @@ def tms_to_geotiff( SESSION = httpx.Client() except ImportError: - import requests - SESSION = requests.Session() from .basemaps import XYZ_TILES @@ -14924,9 +14917,6 @@ def ee_to_geotiff( quiet (bool, optional): Whether to hide the download progress bar. Defaults to False. """ - - import box - image = None if ( @@ -15656,8 +15646,6 @@ def pmtiles_metadata(input_file: str) -> dict[str, str | int | list[str]]: If fetching a remote PMTiles file, this function may perform multiple requests to minimize the amount of data downloaded. """ - import requests - try: from pmtiles.reader import Reader, MmapSource, MemorySource except ImportError: From f2b71cc1b80deb07f3e2714a41b6289cfbfe0334 Mon Sep 17 00:00:00 2001 From: Kurt Schwehr Date: Wed, 31 Dec 2025 09:04:57 -0800 Subject: [PATCH 2/4] common.py: More cleanup * Remove types from doc strings with type annotations are present * Add missing punctation in comments * Fix typo `avariables` * Add type annotations * Remove commented out code * Simplify `get_current_year` * `find_files`: * Switch `Path` to pathlib.Path * Make a local variable `path` for `Path(input_dir)` * `(x, y)` --> `x, y` when the parens are not required * Remove `:param:` in doc string for `get_geometry_coords` * Remove extra blank lines * Remove redundant comments * Start doc string summary line directly after `"""` * Add `assert parent` for `psutil.Process().parent()` for pytype * Avoid a local bool variable when just returning a bool * `hex_to_rgba`: Improve doc string * `remove_port_from_string`: Remove `result = ` when just returning result * `pmtiles_style`: Remove extra kwargs * Use module import: `get_palette` --> `colormaps.get_palette` * Use `pathlib.Path.read()` --- geemap/common.py | 389 ++++++++++++++++++++++------------------------- 1 file changed, 180 insertions(+), 209 deletions(-) diff --git a/geemap/common.py b/geemap/common.py index 50cf0e2ce0..392416c910 100644 --- a/geemap/common.py +++ b/geemap/common.py @@ -24,6 +24,7 @@ import json import math import os +import pathlib import re import shutil import subprocess @@ -1886,7 +1887,7 @@ def check_git_install() -> bool: """Checks if Git is installed. Returns: - bool: Returns True if Git is installed, otherwise returns False. + Returns True if Git is installed, otherwise returns False. """ cmd = "git --version" output = os.popen(cmd).read() @@ -2342,7 +2343,7 @@ def edit_download_html( basename = os.path.basename(filename) - # Create and assign html to widget + # Create and assign html to widget. html = '{title}' htmlWidget.value = html.format( payload=payload, title=title + basename, filename=basename @@ -9234,7 +9235,7 @@ def extract_pixel_values( def list_vars(var_type=None): - """Lists all defined avariables. + """Lists all defined variables. Args: var_type (object, optional): The object type of variables to list. Defaults to None. @@ -11020,21 +11021,20 @@ def bbox_to_gdf(bbox, crs="EPSG:4326"): return gdf -def check_dir(dir_path, make_dirs=True): +def check_dir(dir_path: str, make_dirs: bool = True) -> str: """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. + dir_path: The path to the directory. + make_dirs: 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. + The path to the directory. """ - if isinstance(dir_path, str): if dir_path.startswith("~"): dir_path = os.path.expanduser(dir_path) @@ -11052,19 +11052,19 @@ def check_dir(dir_path, make_dirs=True): raise TypeError("The provided directory path must be a string.") -def check_file_path(file_path, make_dirs=True): +def check_file_path(file_path: str, make_dirs: bool = True) -> str: """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. + file_path: The path to the file. + make_dirs: 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. + The absolute path to the file. """ if isinstance(file_path, str): if file_path.startswith("~"): @@ -14054,14 +14054,14 @@ def arc_add_layer(url, name=None, shown=True, opacity=1.0): layer.transparency = 100 - (opacity * 100) -def arc_zoom_to_extent(xmin, ymin, xmax, ymax): +def arc_zoom_to_extent(xmin: float, ymin: float, xmax: float, ymax: float) -> None: """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. + xmin: The minimum x value of the extent. + ymin: The minimum y value of the extent. + xmax: The maximum x value of the extent. + ymax: The maximum y value of the extent. """ if is_arcpy(): import arcpy @@ -14078,32 +14078,27 @@ def arc_zoom_to_extent(xmin, ymin, xmax, ymax): ) ) - # if isinstance(zoom, int): - # scale = 156543.04 * math.cos(0) / math.pow(2, zoom) - # view.camera.scale = scale # Not working properly - -def get_current_year(): - """Get the current year. +def get_current_year() -> int: + """Returns the current year.""" + return datetime.date.today().year - Returns: - int: The current year. - """ - today = datetime.date.today() - return today.year - -def html_to_gradio(html, width="100%", height="500px", **kwargs): +def html_to_gradio( + html: str | list[str], width: str = "100%", height: str = "500px", **kwargs +) -> str: """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'. + html: The HTML string or list of strings to convert. + width: The width of the map. Defaults to '100%'. + height: The height of the map. Defaults to '500px'. Returns: - str: The HTML string to use in Gradio. + The HTML string to use in Gradio. """ + del kwargs # Unused. if isinstance(width, int): width = f"{width}px" @@ -14175,11 +14170,11 @@ def image_client(image, **kwargs): return client -def image_center(image, **kwargs): +def image_center(image: str, **kwargs): """Get the center of an image. Args: - image (str): The input image filepath or URL. + image: The input image filepath or URL. Returns: tuple: A tuple of (latitude, longitude). @@ -14193,17 +14188,17 @@ def image_center(image, **kwargs): return client.center() -def image_bounds(image, **kwargs): +def image_bounds(image: str, **kwargs) -> list[tuple[float, float]]: """Get the bounds of an image. Args: - image (str): The input image filepath or URL. + image: The input image filepath or URL. Returns: - list: A list of bounds in the form of [(south, west), (north, east)]. + 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: @@ -14212,14 +14207,14 @@ def image_bounds(image, **kwargs): return [(bounds[0], bounds[2]), (bounds[1], bounds[3])] -def image_metadata(image, **kwargs): +def image_metadata(image: str, **kwargs) -> dict[str, Any]: """Get the metadata of an image. Args: - image (str): The input image filepath or URL. + image: The input image filepath or URL. Returns: - dict: A dictionary of image metadata. + A dictionary of image metadata. """ image_check(image) @@ -14230,16 +14225,15 @@ def image_metadata(image, **kwargs): return client.metadata() -def image_bandcount(image, **kwargs): +def image_bandcount(image: str, **kwargs) -> int: """Get the number of bands in an image. Args: - image (str): The input image filepath or URL. + image: The input image filepath or URL. Returns: - int: The number of bands in the image. + The number of bands in the image. """ - image_check(image) if isinstance(image, str): @@ -14249,14 +14243,14 @@ def image_bandcount(image, **kwargs): return len(client.metadata()["bands"]) -def image_size(image, **kwargs): +def image_size(image: str, **kwargs) -> tuple[int, int]: """Get the size (width, height) of an image. Args: - image (str): The input image filepath or URL. + image: The input image filepath or URL. Returns: - tuple: A tuple of (width, height). + A tuple of (width, height). """ image_check(image) @@ -14269,14 +14263,14 @@ def image_size(image, **kwargs): return metadata["sourceSizeX"], metadata["sourceSizeY"] -def image_projection(image, **kwargs): +def image_projection(image: str, **kwargs) -> str: """Get the projection of an image. Args: - image (str): The input image filepath or URL. + image: The input image filepath or URL. Returns: - str: The projection of the image. + The projection of the image. """ image_check(image) @@ -14287,14 +14281,13 @@ def image_projection(image, **kwargs): return client.metadata()["Projection"] -def image_set_crs(image, epsg): +def image_set_crs(image: str, epsg: int) -> None: """Define the CRS of an image. Args: - image (str): The input image filepath - epsg (int): The EPSG code of the CRS to set. + image: The input image filepath + epsg: The EPSG code of the CRS to set. """ - from rasterio.crs import CRS import rasterio @@ -14302,14 +14295,14 @@ def image_set_crs(image, epsg): rds.crs = CRS.from_epsg(epsg) -def image_geotransform(image, **kwargs): +def image_geotransform(image: str, **kwargs) -> list[float]: """Get the geotransform of an image. Args: - image (str): The input image filepath or URL. + image: The input image filepath or URL. Returns: - list: A list of geotransform values. + A list of geotransform values. """ image_check(image) @@ -14320,14 +14313,14 @@ def image_geotransform(image, **kwargs): return client.metadata()["GeoTransform"] -def image_resolution(image, **kwargs): +def image_resolution(image: str, **kwargs) -> float: """Get the resolution of an image. Args: - image (str): The input image filepath or URL. + image: The input image filepath or URL. Returns: - float: The resolution of the image. + The resolution of the image. """ image_check(image) @@ -14338,23 +14331,20 @@ def image_resolution(image, **kwargs): return client.metadata()["GeoTransform"][1] -def find_files(input_dir, ext=None, fullpath=True, recursive=True): - """Find files in a directory. +def find_files( + input_dir: str, + ext: str | None = None, + fullpath: bool = True, + recursive: bool = True, +) -> list[str]: + """Return a list of matching 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. + input_dir: The input directory. + ext: The file extension to match. Defaults to None. + fullpath: Whether to return the full path. Defaults to True. + recursive: Whether to search recursively. Defaults to True. """ - - from pathlib import Path - - files = [] - if ext is None: ext = "*" else: @@ -14362,44 +14352,48 @@ def find_files(input_dir, ext=None, fullpath=True, recursive=True): ext = f"*.{ext}" + files: list[str] + + path = pathlib.Path(input_dir) if recursive: if fullpath: - files = [str(path.joinpath()) for path in Path(input_dir).rglob(ext)] + files = [str(path.joinpath()) for path in path.rglob(ext)] else: - files = [str(path.name) for path in Path(input_dir).rglob(ext)] + files = [str(path.name) for path in path.rglob(ext)] else: if fullpath: - files = [str(path.joinpath()) for path in Path(input_dir).glob(ext)] + files = [str(path.joinpath()) for path in path.glob(ext)] else: - files = [path.name for path in Path(input_dir).glob(ext)] + files = [path.name for path in path.glob(ext)] return files -def zoom_level_resolution(zoom, latitude=0): +def zoom_level_resolution(zoom: int, latitude: float = 0.0) -> float: """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 + + 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. + zoom: The zoom level. + latitude: The latitude. Defaults to 0.0. Returns: - float: Map resolution in meters. + Map resolution in meters. """ resolution = 156543.04 * math.cos(latitude) / math.pow(2, zoom) return abs(resolution) -def lnglat_to_meters(longitude, latitude): +def lnglat_to_meters(longitude: float, latitude: float) -> tuple[float, float]: """coordinate conversion between lat/lon in decimal degrees to web mercator Args: - longitude (float): The longitude. - latitude (float): The latitude. + longitude: The longitude. + latitude: The latitude. Returns: - tuple: A tuple of (x, y) in meters. + A tuple of (x, y) in meters. """ origin_shift = np.pi * 6378137 easting = longitude * origin_shift / 180.0 @@ -14417,18 +14411,18 @@ def lnglat_to_meters(longitude, latitude): else: northing = -20048966 - return (easting, northing) + return easting, northing -def meters_to_lnglat(x, y): +def meters_to_lnglat(x: float, y: float) -> tuple[float, float]: """coordinate conversion between web mercator to lat/lon in decimal degrees Args: - x (float): The x coordinate. - y (float): The y coordinate. + x: The x coordinate. + y: The y coordinate. Returns: - tuple: A tuple of (longitude, latitude) in decimal degrees. + A tuple of (longitude, latitude) in decimal degrees. """ origin_shift = np.pi * 6378137 longitude = (x / origin_shift) * 180.0 @@ -14436,19 +14430,18 @@ def meters_to_lnglat(x, y): latitude = ( 180 / np.pi * (2 * np.arctan(np.exp(latitude * np.pi / 180.0)) - np.pi / 2.0) ) - return (longitude, latitude) + return longitude, latitude -def bounds_to_xy_range(bounds): +def bounds_to_xy_range(bounds) -> tuple[tuple[float, float], tuple[float, float]]: """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). + A tuple of (x_range, y_range). """ - if isinstance(bounds, tuple): bounds = list(bounds) elif not isinstance(bounds, list): @@ -14459,25 +14452,28 @@ def bounds_to_xy_range(bounds): elif len(bounds) == 2: south, west = bounds[0] north, east = bounds[1] + else: + raise ValueError("bounds must be a list of length 4 or 2") xmin, ymin = lnglat_to_meters(west, south) xmax, ymax = lnglat_to_meters(east, north) - x_range = (xmin, xmax) - y_range = (ymin, ymax) + x_range = xmin, xmax + y_range = ymin, ymax return x_range, y_range -def center_zoom_to_xy_range(center, zoom): +def center_zoom_to_xy_range( + center: tuple[float, float], zoom: int +) -> tuple[tuple[float, float], tuple[float, float]]: """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. + center: A tuple of (latitude, longitude). + zoom: The zoom level. Returns: - tuple: A tuple of (x_range, y_range). + A tuple of (x_range, y_range). """ - if isinstance(center, tuple) or isinstance(center, list): pass else: @@ -14509,16 +14505,17 @@ def center_zoom_to_xy_range(center, zoom): return x_range, y_range -def get_geometry_coords(row, geom, coord_type, shape_type, mercator=False): - """ - Returns the coordinates ('x' or 'y') of edges of a Polygon exterior. +def get_geometry_coords( + row, geom: str, coord_type: str, shape_type: str, mercator: bool = False +) -> float | list[float]: + """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 + row (GeoPandas Series): The row of each of the GeoPandas DataFrame. + geom: The column name. + coord_type: Whether it's 'x' or 'y' coordinate. + shape_type: The shape type of the geometry. + mercator: Whether to convert to Web Mercator coordinates. """ - # Parse the exterior of the coordinate if shape_type.lower() in ["polygon", "multipolygon"]: exterior = row[geom].geoms[0].exterior @@ -14566,19 +14563,22 @@ def get_geometry_coords(row, geom, coord_type, shape_type, mercator=False): return coords -def landsat_scaling(image, thermal_bands=True, apply_fmask=False): - """Apply scaling factors to a Landsat image. See an example at +def landsat_scaling( + image, thermal_bands: bool = True, apply_fmask: bool = False +) -> ee.Image: + """Apply scaling factors to a Landsat image. + + Example https://developers.google.com/earth-engine/datasets/catalog/LANDSAT_LC09_C02_T1_L2 Args: image (ee.Image): The input Landsat image. - thermal_bands (bool, optional): Whether to apply scaling to thermal bands. Defaults to True. - apply_fmask (bool, optional): Whether to apply Fmask cloud mask. Defaults to False. + thermal_bands: Whether to apply scaling to thermal bands. Defaults to True. + apply_fmask: Whether to apply Fmask cloud mask. Defaults to False. Returns: - ee.Image: The scaled Landsat image. + The scaled Landsat image. """ - # Apply the scaling factors to the appropriate bands. opticalBands = image.select("SR_B.").multiply(0.0000275).add(-0.2) if thermal_bands: @@ -14638,7 +14638,6 @@ def tms_to_geotiff( """ from PIL import Image - from osgeo import gdal, osr gdal.UseExceptions() @@ -14915,7 +14914,6 @@ def ee_to_geotiff( crs (str, optional): The CRS of the output image. Defaults to "EPSG:3857". to_cog (bool, optional): Whether to convert the image to Cloud Optimized GeoTIFF. Defaults to False. quiet (bool, optional): Whether to hide the download progress bar. Defaults to False. - """ image = None @@ -14985,19 +14983,21 @@ def ee_to_geotiff( tms_to_geotiff(output, bbox, zoom, resolution, url, crs, to_cog, quiet, **kwargs) -def create_grid(ee_object, scale, proj=None): +def create_grid( + ee_object: ee.Image | ee.Geometry | ee.FeatureCollection, + scale: float | Any, + proj: str | Any | None = None, +) -> ee.FeatureCollection: """Create a grid covering an Earth Engine object. Args: - ee_object (ee.Image | ee.Geometry | ee.FeatureCollection): The Earth Engine object. - scale (float): The grid cell size. - proj (str, optional): The projection. Defaults to None. - + ee_object: The Earth Engine object. + scale: The grid cell size. + proj: The projection. Defaults to None. Returns: - ee.FeatureCollection: The grid as a feature collection. + The grid as a feature collection. """ - if isinstance(ee_object, ee.FeatureCollection) or isinstance(ee_object, ee.Image): geometry = ee_object.geometry() elif isinstance(ee_object, ee.Geometry): @@ -15015,13 +15015,10 @@ def create_grid(ee_object, scale, proj=None): return grid -def jslink_slider_label(slider, label): - """Link a slider and a label. - - Args: - slider (ipywidgets.IntSlider | ipywidgets.FloatSlider): The slider. - label (ipywidgets.Label): The label. - """ +def jslink_slider_label( + slider: ipywidgets.IntSlider | ipywidgets.FloatSlider, label: ipywidgets.Label +) -> None: + """Link a slider and a label.""" def update_label(change): if change["name"]: @@ -15030,14 +15027,14 @@ def update_label(change): slider.observe(update_label, "value") -def check_basemap(basemap): +def check_basemap(basemap: str) -> str: """Check Google basemaps Args: - basemap (str): The basemap name. + basemap: The basemap name. Returns: - str: The basemap name. + The basemap name. """ if isinstance(basemap, str): map_dict = { @@ -15073,39 +15070,33 @@ def get_ee_token(): def geotiff_to_image(image: str, output: str) -> None: - """ - Converts a GeoTIFF file to a JPEG/PNG image. + """Converts a GeoTIFF file to a JPEG/PNG image. Args: - image (str): The path to the input GeoTIFF file. - output (str): The path to save the output JPEG/PNG file. + image: The path to the input GeoTIFF file. + output: The path to save the output JPEG/PNG file. Returns: None """ - import rasterio from PIL import Image - # Open the GeoTIFF file with rasterio.open(image) as dataset: - # Read the image data data = dataset.read() - # Convert the image data to 8-bit format (assuming it's not already) + # Convert the image data to 8-bit format (assuming it's not already). if dataset.dtypes[0] != "uint8": data = (data / data.max() * 255).astype("uint8") - # Convert the image data to RGB format if it's a single band image + # Convert the image data to RGB format if it's a single band image. if dataset.count == 1: data = data.squeeze() data = data.reshape((1, data.shape[0], data.shape[1])) data = data.repeat(3, axis=0) - # Create a PIL Image object from the image data image = Image.fromarray(data.transpose(1, 2, 0)) - # Save the image as a JPEG file image.save(output) @@ -15120,8 +15111,7 @@ def xee_to_image( quiet: bool = False, **kwargs, ) -> None: - """ - Convert xarray Dataset to georeferenced images. + """Convert xarray Dataset to georeferenced images. Args: xds (xr.Dataset): The xarray Dataset to convert to images. @@ -15142,7 +15132,6 @@ def xee_to_image( Raises: ValueError: If the number of filenames doesn't match the number of time steps in the Dataset. - """ try: import rioxarray @@ -15466,49 +15455,41 @@ def array_to_image( dst.write(array[:, :, i], i + 1) -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. - """ - +def is_studio_lab() -> bool: + """Returns True if the notebook is running on Studio Lab.""" import psutil - output = psutil.Process().parent().cmdline() + parent = psutil.Process().parent() + assert parent # For pytype. + output = parent.cmdline() - on_studio_lab = False for item in output: if "studiolab/bin" in item: - on_studio_lab = True - return on_studio_lab - + return True + return False -def is_on_aws(): - """Check if the current notebook is running on AWS. - - Returns: - bool: True if the notebook is running on AWS. - """ +def is_on_aws() -> bool: + """Returns True if the notebook is running on AWS.""" import psutil - output = psutil.Process().parent().cmdline() + parent = psutil.Process().parent() + assert parent # For pytype. + output = parent.cmdline() - on_aws = False for item in output: if item.endswith(".aws") or "ec2-user" in item: - on_aws = True - return on_aws + return True + return False -def xarray_to_raster(dataset, filename: str, **kwargs: dict[str, Any]) -> None: +def xarray_to_raster(dataset, filename: str, **kwargs) -> 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. + filename: The output filename for the raster file. + **kwargs: 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: @@ -15541,17 +15522,16 @@ def xarray_to_raster(dataset, filename: str, **kwargs: dict[str, Any]) -> None: def hex_to_rgba(hex_color: str, opacity: float) -> str: - """ - Converts a hex color code to an RGBA color string. + """Converts a hex color code to an RGBA color string. Args: - hex_color (str): The hex color code to convert. It can be in the format - '#RRGGBB' or 'RRGGBB'. - opacity (float): The opacity value for the RGBA color. It should be a + hex_color: The hex color code to convert. It can be in the format + '#RRGGBB' or 'RRGGBB'. Does not support alpha or 'RGB'. + opacity: The opacity value for the RGBA color. It should be a float between 0.0 (completely transparent) and 1.0 (completely opaque). Returns: - str: The RGBA color string in the format 'rgba(R, G, B, A)'. + The RGBA color string in the format 'rgba(R, G, B, A)'. """ hex_color = hex_color.lstrip("#") h_len = len(hex_color) @@ -15578,11 +15558,10 @@ def replace_top_level_hyphens(d: dict | Any) -> dict | Any: def replace_hyphens_in_keys(d: dict | list | Any) -> dict | list | Any: - """ - Recursively replaces hyphens with underscores in dictionary keys. + """Recursively replaces hyphens with underscores in dictionary keys. Args: - d (dict | list | Any): The input dictionary, list or any other data type. + d: The input dictionary, list, or any other data type. Returns: The modified dictionary or list with keys having hyphens replaced with underscores, @@ -15597,26 +15576,22 @@ def replace_hyphens_in_keys(d: dict | list | Any) -> dict | list | Any: def remove_port_from_string(data: str) -> str: - """ - Removes the port number from all URLs in the given string. + """Removes the port number from all URLs in the given string. Args:: - data (str): The input string containing URLs. + data: The input string containing URLs. Returns: - str: The string with port numbers removed from all URLs. + The string with port numbers removed from all URLs. """ - # Regular expression to match URLs with port numbers + # 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 + # 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 + return url_with_port_pattern.sub(remove_port, data) def pmtiles_metadata(input_file: str) -> dict[str, str | int | list[str]]: @@ -15655,7 +15630,7 @@ def pmtiles_metadata(input_file: str) -> dict[str, str | int | list[str]]: return # ignore uri parameters when checking file suffix - if not urlparse(input_file).path.endswith(".pmtiles"): + if not urllib.parse.urlparse(input_file).path.endswith(".pmtiles"): raise ValueError("Input file must be a .pmtiles file.") header = pmtiles_header(input_file) @@ -15698,7 +15673,6 @@ def pmtiles_style( circle_radius: int = 5, line_width: int = 1, attribution: str = "PMTiles", - **kwargs, ): """ Generates a Mapbox style JSON for rendering PMTiles data. @@ -15741,9 +15715,7 @@ def pmtiles_style( elif isinstance(cmap, list): palette = cmap else: - from .colormaps import get_palette - - palette = ["#" + c for c in get_palette(cmap, n_class)] + palette = ["#" + c for c in colormaps.get_palette(cmap, n_class)] n_class = len(palette) @@ -15815,7 +15787,7 @@ def pmtiles_style( return style -def check_html_string(html_string): +def check_html_string(html_string: str) -> str: """Check if an HTML string contains local images and convert them to base64. Args: @@ -15828,12 +15800,11 @@ def check_html_string(html_string): img_regex = r']+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( - f'src="{match}"', - 'src="data:image/png;base64,' + base64_data + '"', - ) + img_data = pathlib.Path(match).read_bytes() + 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 From 4a9322ffd6881e91c5aeada51e7b3dbcf50238f8 Mon Sep 17 00:00:00 2001 From: Kurt Schwehr Date: Wed, 31 Dec 2025 09:17:41 -0800 Subject: [PATCH 3/4] common.py: Fixes for the prior commit * import colormaps * ipywidgets is aliased to widgets --- geemap/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/geemap/common.py b/geemap/common.py index 392416c910..d6b2614fe4 100644 --- a/geemap/common.py +++ b/geemap/common.py @@ -54,6 +54,7 @@ import plotly.express as px import requests +from . import colormaps from .coreutils import * from . import coreutils @@ -15016,7 +15017,7 @@ def create_grid( def jslink_slider_label( - slider: ipywidgets.IntSlider | ipywidgets.FloatSlider, label: ipywidgets.Label + slider: widgets.IntSlider | widgets.FloatSlider, label: widgets.Label ) -> None: """Link a slider and a label.""" From 19ad7ee6bd15db1ca144f3ab32f3ee7c17166ac4 Mon Sep 17 00:00:00 2001 From: Kurt Schwehr Date: Thu, 8 Jan 2026 13:51:31 -0800 Subject: [PATCH 4/4] common.py: Fix extra spacing nits --- geemap/common.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/geemap/common.py b/geemap/common.py index 3552c1c36a..83df132c43 100644 --- a/geemap/common.py +++ b/geemap/common.py @@ -1141,7 +1141,6 @@ def ee_export_vector_to_asset( maxVertices: Max number of uncut vertices per geometry; geometries with more vertices will be cut into pieces smaller than this size. **kwargs: Holds other keyword arguments that may have been deprecated. - """ if not isinstance(collection, ee.FeatureCollection): raise ValueError("The collection must be an ee.FeatureCollection.") @@ -15462,7 +15461,7 @@ def is_studio_lab() -> bool: import psutil parent = psutil.Process().parent() - assert parent # For pytype. + assert parent # For pytype. output = parent.cmdline() for item in output: