diff --git a/docs/maplibre/mapillary.ipynb b/docs/maplibre/mapillary.ipynb
index 485df2b655..e67988b665 100644
--- a/docs/maplibre/mapillary.ipynb
+++ b/docs/maplibre/mapillary.ipynb
@@ -30,6 +30,7 @@
"metadata": {},
"outputs": [],
"source": [
+ "import leafmap.common as common\n",
"import leafmap.maplibregl as leafmap"
]
},
@@ -68,6 +69,63 @@
"source": [
"![image](https://github.com/user-attachments/assets/db9fac4f-4d67-4ccb-8f2d-06d665bdd521)"
]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "image_ids = common.search_mapillary_images(\n",
+ " lon=-73.99941, lat=40.71194, radius=0.0005, limit=5\n",
+ ")\n",
+ "image_ids"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "common.get_mapillary_image_url(image_ids[0], resolution=\"original\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "common.download_mapillary_images(image_ids, resolution=\"original\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "common.get_mapillary_image_widget(image_ids[0], style=\"classic\", width=1000)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "common.get_mapillary_image_widget(image_ids[0], style=\"split\", width=1000)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "common.get_mapillary_image_widget(image_ids[0], style=\"photo\", width=1000)"
+ ]
}
],
"metadata": {
diff --git a/leafmap/common.py b/leafmap/common.py
index f8ce25a1e2..bdc8a805fc 100644
--- a/leafmap/common.py
+++ b/leafmap/common.py
@@ -15355,3 +15355,210 @@ def plot_actual_vs_predicted(
return fig
else:
fig.show()
+
+
+def search_mapillary_images(
+ lon: Optional[float] = None,
+ lat: Optional[float] = None,
+ radius: float = 0.00005,
+ bbox: Optional[Union[str, List[float]]] = None,
+ limit: int = 2000,
+ access_token: Optional[str] = None,
+) -> List[str]:
+ """
+ Retrieves Mapillary image IDs near the specified test point within a bounding box.
+
+ Args:
+ lon (float, optional): Longitude of the test point. Defaults to None.
+ lat (float, optional): Latitude of the test point. Defaults to None.
+ radius (float, optional): Radius to create the bounding box. Defaults to 0.00005.
+ bbox (Union[str, List[float]], optional): Bounding box coordinates. Defaults to None.
+ limit (int, optional): Maximum number of image IDs to retrieve. Defaults to 2000.
+ access_token (str, optional): Mapillary API access token. Defaults to None.
+
+ Returns:
+ List[str]: JSON response from the Mapillary API containing image IDs.
+ """
+
+ if access_token is None:
+ access_token = get_api_key("MAPILLARY_API_KEY")
+
+ if access_token is None:
+ raise ValueError(
+ "Mapillary API access token is required. Set it using the 'access_token' parameter."
+ )
+
+ metadata_endpoint = "https://graph.mapillary.com"
+ headers = {"Authorization": f"OAuth {access_token}"}
+
+ if bbox is None:
+ if lon is None or lat is None:
+ raise ValueError("Longitude and latitude are required.")
+ bbox = f"{lon - radius},{lat - radius},{lon + radius},{lat + radius}"
+ else:
+ if isinstance(bbox, list):
+ bbox = ",".join(str(x) for x in bbox)
+
+ # Construct the bounding box for the API call
+ url_imagesearch = f"{metadata_endpoint}/images?fields=id&bbox={bbox}&limit={limit}"
+
+ try:
+ response = requests.get(url_imagesearch, headers=headers)
+ response.raise_for_status() # Raise an HTTPError for bad responses
+ return [image["id"] for image in response.json()["data"]]
+ except requests.exceptions.RequestException as e:
+ print(f"An error occurred: {e}")
+ return []
+
+
+def get_mapillary_image_widget(
+ image_id: str,
+ style: str = "photo",
+ width: int = 800,
+ height: int = 600,
+ frame_border: int = 0,
+ **kwargs: Any,
+) -> widgets.HTML:
+ """
+ Creates an iframe widget to display a Mapillary image.
+
+ Args:
+ image_id (str): The ID of the Mapillary image.
+ style (str): The style of the image. Can be "photo", "classic", "split". Defaults to "photo".
+ width (int): The width of the iframe. Defaults to 800.
+ height (int): The height of the iframe. Defaults to 600.
+ frame_border (int): The frame border of the iframe. Defaults to 0.
+ **kwargs: Additional keyword arguments for the widget.
+
+ Returns:
+ widgets.HTML: An iframe widget displaying the Mapillary image.
+ """
+
+ content = f"""
+
+ """
+
+ # Create an iframe widget
+ iframe = widgets.HTML(value=content, placeholder="Mapillary Image", **kwargs)
+
+ return iframe
+
+
+def get_mapillary_image_url(
+ image_id: str,
+ resolution: str = "original",
+ access_token: Optional[str] = None,
+ **kwargs: Any,
+) -> Optional[str]:
+ """
+ Retrieves the URL of a Mapillary image.
+
+ Args:
+ image_id (str): The ID of the Mapillary image.
+ resolution (str): The resolution of the image. Can be 256, 1024, 2048, or original.
+ Defaults to "original".
+ access_token (str, optional): The access token for the Mapillary API. Defaults to None.
+ **kwargs: Additional keyword arguments for the request.
+
+ Raises:
+ ValueError: If no access token is provided.
+
+ Returns:
+ Optional[str]: The URL of the Mapillary image, or None if an error occurs.
+ """
+ if access_token is None:
+ access_token = get_api_key("MAPILLARY_API_KEY")
+
+ if access_token is None:
+ raise ValueError(
+ "Mapillary API access token is required. Set it using the 'access_token' parameter."
+ )
+
+ # API URL
+ url = f"https://graph.mapillary.com/{image_id}"
+
+ # Fields to retrieve
+ fields = f"thumb_{resolution}_url"
+
+ # Request parameters
+ params = {"fields": fields, "access_token": access_token}
+
+ # Fetch the data
+ response = requests.get(url, params=params, **kwargs)
+
+ # Check the response
+ if response.status_code == 200:
+ data = response.json()
+ image_url = data.get(fields)
+ return image_url
+ else:
+ print(f"Error {response.status_code}: {response.text}")
+ return None
+
+
+def download_mapillary_image(
+ image_id: str,
+ output: Optional[str] = None,
+ resolution: str = "original",
+ access_token: Optional[str] = None,
+ quiet: bool = True,
+ **kwargs: Any,
+) -> None:
+ """
+ Downloads a Mapillary image.
+
+ Args:
+ image_id (str): The ID of the Mapillary image.
+ output (str, optional): The output file path. Defaults to None.
+ resolution (str): The resolution of the image. Can be 256, 1024, 2048, or original.
+ Defaults to "original".
+ access_token (str, optional): The access token for the Mapillary API. Defaults to None.
+ quiet (bool): Whether to suppress output. Defaults to True.
+ **kwargs: Additional keyword arguments for the download.
+
+ Returns:
+ None
+ """
+
+ image_url = get_mapillary_image_url(
+ image_id, resolution=resolution, access_token=access_token
+ )
+ if output is None:
+
+ output = f"{image_id}.jpg"
+ download_file(image_url, output, quiet=quiet, **kwargs)
+
+
+def download_mapillary_images(
+ image_ids: List[str],
+ output_dir: Optional[str] = None,
+ resolution: str = "original",
+ **kwargs: Any,
+) -> None:
+ """
+ Downloads multiple Mapillary images.
+
+ Args:
+ image_ids (List[str]): A list of Mapillary image IDs.
+ output_dir (str, optional): The directory to save the images. Defaults
+ to the current working directory.
+ resolution (str): The resolution of the images. Defaults to "original".
+ **kwargs: Additional keyword arguments for the download.
+
+ Returns:
+ None
+ """
+ if output_dir is None:
+ output_dir = os.getcwd()
+
+ for index, image_id in enumerate(image_ids):
+ output = os.path.join(output_dir, f"{image_id}.jpg")
+ print(f"Downloading {index + 1}/{len(image_ids)}: {image_id}.jpg ...")
+ download_mapillary_image(
+ image_id=image_id, output=output, resolution=resolution, **kwargs
+ )
diff --git a/leafmap/maplibregl.py b/leafmap/maplibregl.py
index 848d536a16..cb09b1472b 100644
--- a/leafmap/maplibregl.py
+++ b/leafmap/maplibregl.py
@@ -4018,6 +4018,103 @@ def add_mapillary(
self.add_popup(sequence_lyr_name)
self.add_popup(image_lyr_name)
+ def create_mapillary_widget(
+ self,
+ lon: Optional[float] = None,
+ lat: Optional[float] = None,
+ radius: float = 0.00005,
+ bbox: Optional[Union[str, List[float]]] = None,
+ image_id: Optional[str] = None,
+ style: str = "classic",
+ width: int = 560,
+ height: int = 600,
+ frame_border: int = 0,
+ link: bool = True,
+ container: bool = True,
+ column_widths: List[int] = [8, 1],
+ **kwargs: Any,
+ ) -> Union[widgets.HTML, v.Row]:
+ """
+ Creates a Mapillary widget.
+
+ Args:
+ lon (Optional[float]): Longitude of the location. Defaults to None.
+ lat (Optional[float]): Latitude of the location. Defaults to None.
+ radius (float): Search radius for Mapillary images. Defaults to 0.00005.
+ bbox (Optional[Union[str, List[float]]]): Bounding box for the search. Defaults to None.
+ image_id (Optional[str]): ID of the Mapillary image. Defaults to None.
+ style (str): Style of the Mapillary image. Defaults to "classic".
+ width (int): Width of the iframe. Defaults to 560.
+ height (int): Height of the iframe. Defaults to 600.
+ frame_border (int): Frame border of the iframe. Defaults to 0.
+ link (bool): Whether to link the widget to map clicks. Defaults to True.
+ container (bool): Whether to return the widget in a container. Defaults to True.
+ column_widths (List[int]): Widths of the columns in the container. Defaults to [8, 1].
+ **kwargs: Additional keyword arguments for the widget.
+
+ Returns:
+ Union[widgets.HTML, v.Row]: The Mapillary widget or a container with the widget.
+ """
+
+ if image_id is None:
+ if lon is None or lat is None:
+ if len(self.center) > 0:
+ lon = self.center["lng"]
+ lat = self.center["lat"]
+ else:
+ lon = 0
+ lat = 0
+ image_ids = common.search_mapillary_images(lon, lat, radius, bbox, limit=1)
+ if len(image_ids) > 0:
+ image_id = image_ids[0]
+
+ if image_id is None:
+ widget = widgets.HTML()
+ else:
+ widget = common.get_mapillary_image_widget(
+ image_id, style, width, height, frame_border, **kwargs
+ )
+
+ if link:
+
+ def log_lng_lat(lng_lat):
+ lon = lng_lat.new["lng"]
+ lat = lng_lat.new["lat"]
+ image_id = common.search_mapillary_images(
+ lon, lat, radius=radius, limit=1
+ )
+ if len(image_id) > 0:
+ content = f"""
+
+ """
+ widget.value = content
+
+ self.observe(log_lng_lat, names="clicked")
+
+ if container:
+ left_col_layout = v.Col(
+ cols=column_widths[0],
+ children=[self],
+ class_="pa-1", # padding for consistent spacing
+ )
+ right_col_layout = v.Col(
+ cols=column_widths[1],
+ children=[widget],
+ class_="pa-1", # padding for consistent spacing
+ )
+ row = v.Row(
+ children=[left_col_layout, right_col_layout],
+ )
+ return row
+ else:
+
+ return widget
+
class Container(v.Container):
@@ -5101,3 +5198,27 @@ def on_change(change):
filepath_widget.value = filepaths[0]
return main_widget
+
+
+class MapWidget(v.Row):
+
+ def __init__(self, left_obj, right_obj, column_widths=(5, 1), **kwargs):
+
+ self.left_obj = left_obj
+ self.right_obj = right_obj
+
+ left_col_layout = v.Col(
+ cols=column_widths[0],
+ children=[left_obj],
+ class_="pa-1", # padding for consistent spacing
+ )
+ right_col_layout = v.Col(
+ cols=column_widths[1],
+ children=[right_obj],
+ class_="pa-1", # padding for consistent spacing
+ )
+
+ # if "class_" not in kwargs:
+ # kwargs["class_"] = "d-flex flex-wrap"
+
+ super().__init__(children=[left_col_layout, right_col_layout], **kwargs)