diff --git a/HISTORY.rst b/HISTORY.rst index 37cbd7f..d37cabc 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,4 +5,12 @@ History 0.1.0 (2023-01-19) ------------------ -* First release on PyPI. +* First release (beta). + + +0.2.0 (2023-05-19) +------------------ + +* First public release (beta). + + diff --git a/MANIFEST.in b/MANIFEST.in index d45689c..49ae174 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,7 @@ include HISTORY.rst include LICENSE include README.rst include stac_ipyleaflet/data/* +include stac_ipyleaflet/widgets/* include stac_ipyleaflet/stac_discovery/catalogs/* recursive-include tests * diff --git a/README.md b/README.md index 74afa51..eb137fe 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # STAC ipyleaflet -WORK IN PROGRESS. Right now this does nothing with STAC. Currently this provides a module on top of ipyleaflet demonstrating how to load tile layers (from `biomass-layers.csv` and create histograms from a bounding box and visible layers. +WORK IN PROGRESS. Right now this connects to the MAAP STAC - providing a module on top of ipyleaflet demonstrating how to load & control opacity for tile layers (from `biomass-layers.csv`), view pre-determined Basemaps, and derive coordinates from a user-defined bounding box. -Much of this is inspired and copied from [leafmap](https://leafmap.org/) +Much of this project is inspired from [leafmap](https://leafmap.org/) ![Jupyter Lab ScreenShot](jlab-screenshot.png) @@ -24,3 +24,6 @@ jupyter lab Note this library currently includes `rio.open` so must be run with an AWS identity that has access to the bucket the biomass products are in. +**Styling Notes** +- By default, ipywidget icons can be set to any from the font-awesome library v4: https://fontawesome.com/v4/icons/ +- By default, ipywidget buttons can be styled to any html colors: https://htmlcolorcodes.com/color-names/ diff --git a/README.rst b/README.rst index bfcf2ea..4300feb 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@ ipyleaflet customized for discovering, visualizing and interacting with STAC and * Free software: MIT license -* Documentation: https://stac-ipyleaflet.readthedocs.io. +* Documentation: https://docs.maap-project.org/en/latest/technical_tutorials/visualization/stac_ipyleaflet.html. Features diff --git a/demo.ipynb b/demo.ipynb index 1ae9fd1..1c309fa 100644 --- a/demo.ipynb +++ b/demo.ipynb @@ -1,74 +1,109 @@ { "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "4f7d5321-d82b-4ed4-8136-832f932b30b5", - "metadata": {}, - "outputs": [ - { - "ename": "ImportError", - "evalue": "libpoppler.so.126: cannot open shared object file: No such file or directory", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mImportError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[1], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mstac_ipyleaflet\u001b[39;00m\n", - "File \u001b[0;32m/workspaces/stac_ipyleaflet/stac_ipyleaflet/__init__.py:7\u001b[0m\n\u001b[1;32m 4\u001b[0m __email__ \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124maimee@developmentseed.org\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[1;32m 5\u001b[0m __version__ \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m0.1.0\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[0;32m----> 7\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcore\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;241m*\u001b[39m\n", - "File \u001b[0;32m/workspaces/stac_ipyleaflet/stac_ipyleaflet/core.py:15\u001b[0m\n\u001b[1;32m 13\u001b[0m \u001b[38;5;66;03m#from pydantic import BaseModel\u001b[39;00m\n\u001b[1;32m 14\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mshapely\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mgeometry\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Polygon\n\u001b[0;32m---> 15\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mrioxarray\u001b[39;00m\n\u001b[1;32m 16\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mxarray\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mxr\u001b[39;00m\n\u001b[1;32m 17\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mnumpy\u001b[39;00m\n", - "File \u001b[0;32m/opt/conda/lib/python3.10/site-packages/rioxarray/__init__.py:5\u001b[0m\n\u001b[1;32m 2\u001b[0m __author__ \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\"\"\u001b[39m\u001b[38;5;124mrioxarray Contributors\u001b[39m\u001b[38;5;124m\"\"\"\u001b[39m\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mimportlib\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mmetadata\u001b[39;00m\n\u001b[0;32m----> 5\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mrioxarray\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mraster_array\u001b[39;00m \u001b[38;5;66;03m# noqa\u001b[39;00m\n\u001b[1;32m 6\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mrioxarray\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mraster_dataset\u001b[39;00m \u001b[38;5;66;03m# noqa\u001b[39;00m\n\u001b[1;32m 7\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mrioxarray\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01m_io\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m open_rasterio \u001b[38;5;66;03m# noqa\u001b[39;00m\n", - "File \u001b[0;32m/opt/conda/lib/python3.10/site-packages/rioxarray/raster_array.py:19\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mtyping\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Any, Literal, Optional, Union\n\u001b[1;32m 18\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mnumpy\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mnp\u001b[39;00m\n\u001b[0;32m---> 19\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mrasterio\u001b[39;00m\n\u001b[1;32m 20\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mrasterio\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mmask\u001b[39;00m\n\u001b[1;32m 21\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mrasterio\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mwarp\u001b[39;00m\n", - "File \u001b[0;32m/opt/conda/lib/python3.10/site-packages/rasterio/__init__.py:28\u001b[0m\n\u001b[1;32m 24\u001b[0m os\u001b[38;5;241m.\u001b[39madd_dll_directory(os\u001b[38;5;241m.\u001b[39mpath\u001b[38;5;241m.\u001b[39mabspath(p))\n\u001b[1;32m 27\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mrasterio\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01m_show_versions\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m show_versions\n\u001b[0;32m---> 28\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mrasterio\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01m_version\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m gdal_version, get_geos_version, get_proj_version\n\u001b[1;32m 29\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mrasterio\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcrs\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m CRS\n\u001b[1;32m 30\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mrasterio\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mdrivers\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m driver_from_extension, is_blacklisted\n", - "\u001b[0;31mImportError\u001b[0m: libpoppler.so.126: cannot open shared object file: No such file or directory" - ] - } - ], - "source": [ - "import stac_ipyleaflet" - ] - }, { "cell_type": "markdown", "id": "9648b56f-dbf3-4aac-af19-643067596220", - "metadata": {}, + "metadata": { + "tags": [] + }, "source": [ - "# Use ipyleaflet to visually explore MAAP data\n", + "# Use stac_ipyleaflet to visually explore MAAP data\n", "\n", - "Inspired by leafmap." + "Inspired by Leafmap." ] }, { "cell_type": "code", - "execution_count": 2, - "id": "8e4a59f2-1aa6-4b82-86e5-90d0c3c7ffe2", - "metadata": {}, + "execution_count": 1, + "id": "4f7d5321-d82b-4ed4-8136-832f932b30b5", + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "3a03ad2140da4cd198721f5ba3e089c6", + "model_id": "a889bd5030ec400ebf2bd0ee01bded38", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "StacIpyleaflet(center=[20, 0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zo…" + "HBox(children=(ToggleButton(value=False, description='Layers', icon='map-o', layout=Layout(border_bottom='1px …" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "03a7d6cea86b4f2ab0de29781bb1942c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" ] }, "metadata": {}, "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "a0f8457e810d46f284779d9d6b201f24", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "20b7d3527fcf4a4384ffd3df79e25d44", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "StacIpyleaflet(center=[20, 0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zo…" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "import ipywidgets\n", - "m = stac_ipyleaflet.StacIpyleaflet(zoom=4, layout=ipywidgets.Layout(height=\"800px\"))\n", - "m.draw_biomass_map()\n", + "import stac_ipyleaflet\n", + "# from ipywidgets import Layout\n", + "m = stac_ipyleaflet.StacIpyleaflet()\n", "m" ] }, { "cell_type": "code", "execution_count": null, - "id": "a6b9c37b-58d9-4d17-8891-8eed125eb996", + "id": "d7a3e19b-6e6c-4a4d-95ef-b253d28af4ad", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d2c9bb0-8435-449e-bf44-03e628556239", "metadata": {}, "outputs": [], "source": [] @@ -90,7 +125,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.10.10" } }, "nbformat": 4, diff --git a/requirements.txt b/requirements.txt index 956469d..c8f691e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -ipyleaflet==0.17.2 -ipywidgets==8.0.4 +ipyleaflet>=0.17.2 +ipywidgets>=8.0.4 ipyevents matplotlib pydantic diff --git a/setup.cfg b/setup.cfg index 2f3da0e..bfccf40 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,16 +1,16 @@ [metadata] name = stac_ipyleaflet -version=0.1.0 +version=0.2.0 author = Aimee Barciauskas -description=ipyleaflet customized for discovering, visualizing and interacting with STAC and workspace data. +description=ipyleaflet customized for discovering, visualizing and interacting with STAC data. long_description = file: README.md url = https://github.com/abarciauskas-bgse/stac_ipyleaflet keywords = stac_ipyleaflet author_email=aimee@developmentseed.org license=MIT license classifiers = - Development Status :: 2 - Pre-Alpha - Intended Audience :: Developers + Development Status :: 2 - Beta + Intended Audience :: NASA Scientists License :: OSI Approved :: MIT License Natural Language :: English Programming Language :: Python :: 3.8 @@ -18,7 +18,7 @@ classifiers = Programming Language :: Python :: 3.10 [bumpversion] -current_version = 0.1.0 +current_version = 0.2.0 commit = True tag = True @@ -44,8 +44,8 @@ include_package_data=True zip_safe=False tests_require=[pytest>=3] install_requires = - ipyleaflet==0.17.2 - ipywidgets==8.0.4 + ipyleaflet>=0.17.2 + ipywidgets>=8.0.4 ipyevents matplotlib pydantic diff --git a/stac_ipyleaflet/__init__.py b/stac_ipyleaflet/__init__.py index 107d8a6..c2d6487 100644 --- a/stac_ipyleaflet/__init__.py +++ b/stac_ipyleaflet/__init__.py @@ -1,7 +1 @@ -"""Top-level package for stac ipyleaflet.""" - -__author__ = """Aimee Barciauskas""" -__email__ = 'aimee@developmentseed.org' -__version__ = '0.1.0' - -from .core import * +from stac_ipyleaflet.core import * diff --git a/stac_ipyleaflet/core.py b/stac_ipyleaflet/core.py index eaa7c54..a77a3f3 100644 --- a/stac_ipyleaflet/core.py +++ b/stac_ipyleaflet/core.py @@ -1,168 +1,341 @@ """Main module.""" -import os import csv -from io import BytesIO -import re -import requests - -from ipyleaflet import Map, DrawControl, WidgetControl, TileLayer, Popup -from stac_ipyleaflet.stac_discovery.stac_widget import StacDiscoveryWidget from importlib.resources import files +from ipyleaflet import Map, DrawControl, Popup, TileLayer, WidgetControl from IPython.display import display -import ipywidgets -from ipywidgets import HTML +from ipywidgets import Box, HBox, VBox, Layout, SelectionSlider, HTML, IntSlider, Image +from ipywidgets import Checkbox, Dropdown, Tab, ToggleButton, Button +from ipywidgets import HTML, Output, jslink import matplotlib.pyplot as plt -#from pydantic import BaseModel -from shapely.geometry import Polygon -import rioxarray -import xarray as xr import numpy -import numpy.ma as ma +import re +import requests from rio_tiler.io import Reader from rio_tiler.mosaic import mosaic_reader from rio_tiler.models import ImageData +import rioxarray +from shapely.geometry import Polygon +import xarray as xr + +from stac_ipyleaflet.stac_discovery.stac_widget import StacDiscoveryWidget +from stac_ipyleaflet.widgets.basemaps import BasemapsWidget +from stac_ipyleaflet.widgets.draw import DrawControlWidget class StacIpyleaflet(Map): """ Stac ipyleaflet is an extension to ipyleaflet `Map`. """ + + raw_input = input + draw_control: DrawControl - # TODO(aimee): right now this is specific to MAAP but we should make it generic. - titiler_endpoint: str = "https://titiler.maap-project.org" - titiler_stac_endpoint: str = "https://titiler-stac.maap-project.org" histogram_layer: Popup warning_layer: Popup = None loading_widget_layer: Popup = None bbox_centroid: list = [] + titiler_endpoint = "https://titiler.maap-project.org" + titiler_stac_endpoint = "https://titiler-stac.maap-project.org" + def __init__(self, **kwargs): if "center" not in kwargs: kwargs["center"] = [20, 0] if "zoom" not in kwargs: - kwargs["zoom"] = 2 + kwargs["zoom"] = 4 + + if "layout" not in kwargs: + kwargs["layout"] = Layout(height="600px") + + if "scroll_wheel_zoom" not in kwargs: + kwargs["scroll_wheel_zoom"] = True # Create map - super().__init__(**kwargs) + super().__init__(**kwargs) + + self.accent_color = "SteelBlue" + self.layers = BasemapsWidget.template(self) - # Add rectangle draw control for bounding box - # TODO(aimee): Remove the other draw controls + self.buttons = {} self.selected_data = [] - self.draw_control = None self.histogram_layer = None - draw_control = DrawControl( - edit=True, - remove=True, - ) - draw_control.rectangle = { - "shapeOptions": { - "fillColor": "transparent", - "color": "#333", - "fillOpacity": 1.0 - } - } - self.add_control(draw_control) - self.draw_control = draw_control - + self.draw_control_added = False + self.aoi_coordinates = [] + self.aoi_bbox = () + gif_file = files('stac_ipyleaflet.data').joinpath('loading.gif') with open(gif_file, "rb") as f: - gif_widget=ipywidgets.Image( + gif_widget=Image( value=f.read(), format='png', width=200, height=200, ) - loading_widget=ipywidgets.VBox() + loading_widget=VBox() loading_widget.children=[gif_widget] - loading_location = self.bbox_centroid or self.center self.loading_widget_layer = Popup(child=loading_widget, min_width=200, min_height=200) + main_button_layout = Layout(width="120px", height="35px", border="1px solid #4682B4") + draw_btn = ToggleButton(description="Draw", icon="square-o", layout=main_button_layout) + draw_btn.style.text_color = self.accent_color + draw_btn.style.button_color = "transparent" + draw_btn.tooltip = "Draw Area of Interest" + draw_btn.observe(self.toggle_draw_widget_display, type="change", names=["value"]) + self.buttons["draw"] = draw_btn + layers_btn = ToggleButton(description="Layers", icon="map-o", layout=main_button_layout) + layers_btn.style.text_color = self.accent_color + layers_btn.style.button_color = "transparent" + layers_btn.tooltip = "Open/Close Layers Menu" + layers_btn.observe(self.toggle_layers_widget_display, type="change", names=["value"]) + self.buttons["layers"] = layers_btn + """ histogram_btn = Button(description="Histogram", icon="bar-chart") + histogram_btn.style.text_color = "white" + histogram_btn.style.button_color = self.accent_color + histogram_btn.on_click(self.create_histograms) """ + stac_btn = ToggleButton(description="STAC Data", icon="search", layout=main_button_layout) + stac_btn.style.text_color = self.accent_color + stac_btn.style.button_color = "white" + stac_btn.tooltip = "Open/Close STAC Data Search" + stac_btn.observe(self.toggle_stac_widget_display, type="change", names=["value"]) + self.buttons["stac"] = stac_btn + + buttons_box_layout = Layout(display='flex', + flex_flow='row', + align_items='center', + justify_content='center', + width='100%', + height="50px") + buttons_box = HBox(children=[draw_btn, layers_btn, stac_btn],layout=buttons_box_layout) + display(buttons_box) + + self.add_biomass_layers() + self.add_custom_tools() + self.draw_control = DrawControlWidget.template(self) + return None - def layers_button_clicked(self, b): - layers_widget = self.layers_widget - if layers_widget.layout.display == 'none': - layers_widget.layout.display = 'block' - elif layers_widget.layout.display == 'block': - layers_widget.layout.display = 'none' - - def stac_widget_display(self, b): - stac_widget = self.stac_widget - if stac_widget.layout.display == 'none': - stac_widget.layout.display = 'block' - elif stac_widget.layout.display == 'block': - stac_widget.layout.display = 'none' - - def add_layers_widget(self): - # Adds a list of layers to toggle on and off via checkboxes - layers_widget = ipywidgets.VBox() - layers_hbox = [] - - for layer in self.layers: - layer_chk = ipywidgets.Checkbox( - value=layer.visible, - description=layer.name, - indent=False - ) - ipywidgets.jslink((layer_chk, "value"), (layer, "visible")) - hbox = ipywidgets.HBox( - [layer_chk] - ) - layers_hbox.append(hbox) - layers_widget.children = layers_hbox + #logic to handle main menu toggle buttons + def toggle_layers_widget_display(self, b): + if b["new"]: + if self.layers_widget.layout.display == 'none': + self.layers_widget.layout.display = 'block' + self.stac_widget.layout.display = 'none' + self.aoi_widget.layout.display = 'none' + self.buttons["stac"].value = False + self.buttons["draw"].value = False + if self.draw_control_added: + self.remove(self.draw_control) + if not b["new"]: + if self.layers_widget.layout.display == 'block': + self.layers_widget.layout.display = 'none' + + def toggle_stac_widget_display(self, b): + if b["new"]: + if self.stac_widget.layout.display == 'none': + self.stac_widget.layout.display = 'block' + self.layers_widget.layout.display = 'none' + self.aoi_widget.layout.display = 'none' + self.buttons["layers"].value = False + self.buttons["draw"].value = False + if self.draw_control_added: + self.remove(self.draw_control) + if not b["new"]: + if self.stac_widget.layout.display == 'block': + self.stac_widget.layout.display = 'none' + + def toggle_draw_widget_display(self, b): + if b["new"]: + if self.aoi_widget.layout.display == 'none': + self.aoi_widget.layout.display = 'block' + self.add_control(self.draw_control) + self.draw_control_added = True + self.stac_widget.layout.display = 'none' + self.layers_widget.layout.display = 'none' + self.buttons["stac"].value = False + self.buttons["layers"].value = False + if not b["new"]: + if self.aoi_widget.layout.display == 'block': + self.aoi_widget.layout.display = 'none' + if self.draw_control_added: + self.remove(self.draw_control) + self.draw_control_added = False + + def create_aoi_widget(self): + aoi_widget = HBox(layout=Layout(width="300px", padding="0px 6px 2px 6px", margin="0px 2px 2px 2px")) + aoi_widget.layout.flex_flow="column" + aoi_widget.layout.min_width="300px" + aoi_widget.layout.max_height="360px" + aoi_widget.layout.overflow="auto" + + aoi_widget_desc = HTML( + value="

Polygon

", + ) + aoi_html = HTML( + value="Waiting for area of interest...", + description="", + ) + aoi_clear_button = Button( + description="Clear AOI Polygon", + tooltip="Clear AOI Polygon", + icon="trash", + disabled=True, + # layout=Layout(margin="4px 0 8px 0") + ) + + aoi_widget.children = [aoi_widget_desc, aoi_html, aoi_clear_button] + aoi_widget.layout.display ='none' + + return aoi_widget + + def create_layers_widget(self): + + layers_widget = Box(style= { "max-width: 420px" }) + layers_widget.layout.flex_flow="column" + layers_widget.layout.max_height="360px" + layers_widget.layout.overflow="auto" + + tab_headers = ['Biomass Layers', 'Basemaps'] + tab_children = [] + tab_widget = Tab() + + out = Output() + display(out) + + + opacity_values = [i*10 for i in range(10+1)] # [0.001, 0.002, ...] + + def handle_basemap_opacity_change(change): + selected_bm = self.basemap_selection_dd.value + for l in self.layers: + if l.base: + if l.name == selected_bm: + l.opacity = change['new']/100 + + def handle_layer_opacity_change(change): + selected_layer = change.owner.description + for l in self.layers: + if l.name == selected_layer: + l.opacity = change['new'] + + for tab in tab_headers: + tab_content = VBox() + listed_layers = [] + # sort layers by name property + layers_in_drawing_order = [l for l in self.layers] + layerlist_layers = sorted(layers_in_drawing_order, key=lambda x: x.name, reverse=False) + if tab == "Biomass Layers": + layers_hbox = [] + for layer in layerlist_layers: + # check if layer name is a basemap + if not layer.base: + layer_checkbox = Checkbox( + value=layer.visible, + description=layer.name, + indent=False + ) + jslink((layer_checkbox, "value"), (layer, "visible")) + hbox = HBox( + [layer_checkbox] + ) + layer_opacity_slider = SelectionSlider( + value=1, + options=[("%g"%i, i/100) for i in opacity_values], + description=f"{layer.name}", + continuous_update=False, + orientation='horizontal', + layout=Layout(margin="-12px 0 4px 0") + ) + layer_opacity_slider.style.description_width = "0px" + layer_opacity_slider.style.handle_color = self.accent_color + layer_opacity_slider.observe(handle_layer_opacity_change, names="value") + layers_hbox.append(hbox) + layers_hbox.append(layer_opacity_slider) + listed_layers.append(layer.name) + tab_content.children = [VBox(layers_hbox)] + tab_children.append(tab_content) + elif tab == "Basemaps": + basemaps = [] + for layer in layerlist_layers: + # check if layer is a basemap + if layer.base: + basemaps.append((f"{layer.name}", f"{layer.name}")) + + def on_change(change): + if change['type'] == 'change' and change['name'] == 'value': + with out: + out.clear_output() + #print("changed to %s" % change['new']) + for l in self.layers: + if l.base: + if l.name == change['new']: + l.opacity = basemap_opacity_slider.value/100 + l.visible = True + else: + l.visible = False + return + dropdown = Dropdown(options=basemaps, value="Open Street Map") + self.basemap_selection_dd = dropdown + dropdown.observe(on_change) + + basemap_opacity_slider = IntSlider( + value=100, + min=0, + max=100, + step=10, + description='% Opacity:', + #disabled=False, + style={'bar_color': 'maroon'}, + continuous_update=False, + orientation='horizontal', + readout=True, + readout_format='d' + ) + + basemap_opacity_slider.style.handle_color = self.accent_color + basemap_opacity_slider.observe(handle_basemap_opacity_change, names="value") + tab_content.children = [dropdown, basemap_opacity_slider] + tab_children.append(tab_content) + + tab_widget.children = tab_children + tab_widget.titles = tab_headers + print(tab_widget.box_style) + layers_widget.children = [tab_widget] layers_widget.layout.display ='none' return layers_widget + - def add_toolbar(self): - # Add a widget for the layers - self.layers_widget = self.add_layers_widget() + def add_custom_tools(self): + # Create custom map widgets + self.layers_widget = self.create_layers_widget() self.stac_widget = StacDiscoveryWidget.template(self) + self.aoi_widget = self.create_aoi_widget() - # Add a button to toggle the layers checkbox widget on and off - stac_widget_button = ipywidgets.Button( - tooltip="STAC Discovery", - icon="stack-exchange", - layout=ipywidgets.Layout(height="28px", width="38px"), - ) - stac_widget_button.on_click(self.stac_widget_display) - - layers_button = ipywidgets.Button( - tooltip="Open Layers List", - icon="map-o", - layout=ipywidgets.Layout(height="28px", width="38px"), - ) + layers_widget = VBox([self.layers_widget]) + stac_widget = VBox([self.stac_widget]) + aoi_widget = VBox([self.aoi_widget]) - layers_button.on_click(self.layers_button_clicked) - - hist_button = ipywidgets.Button( - tooltip="Create histogram", - icon="bar-chart", - layout=ipywidgets.Layout(height="28px", width="38px"), - ) - hist_button.on_click(self.create_histograms) - toolbar_widget = ipywidgets.VBox() - toolbar_widget.children = [layers_button, hist_button, self.layers_widget] - toolbar_control = WidgetControl(widget=toolbar_widget, position="topright") - self.add(toolbar_control) - - stac_widget = ipywidgets.VBox() - stac_widget.children = [stac_widget_button, self.stac_widget] - self.add(WidgetControl(widget=stac_widget, position="topright")) + layers_control = WidgetControl(widget=layers_widget, position="topright", id="layers_widget") + stack_control = WidgetControl(widget=stac_widget, position="topright", id="stac_widget") + aoi_control = WidgetControl(widget=aoi_widget, position="topright", id="aoi_widget") + + self.add(layers_control) + self.add(stack_control) + self.add(aoi_control) def add_biomass_layers(self): biomass_file = files('stac_ipyleaflet.data').joinpath('biomass-layers.csv') with open(biomass_file, newline='') as f: csv_reader = csv.reader(f) next(csv_reader, None) # skip the headers - for row in csv_reader: + sorted_csv = sorted(csv_reader, key=lambda row: row[0], reverse=True) + for row in sorted_csv: name, tile_url = row[0], row[1] tile_layer = TileLayer(url=tile_url, attribution=name, name=name, visible=False) self.add_layer(tile_layer) def find_layer(self, name: str): layers = self.layers - for layer in layers: if layer.name == name: return layer @@ -210,7 +383,7 @@ def add_tile_layer( print("Failed to add the specified TileLayer.") raise Exception(e) - def gen_mosaic_dataset_reader(self, assets, bounds): + """ def gen_mosaic_dataset_reader(self, assets, bounds): # see https://github.com/cogeotiff/rio-tiler/blob/main/rio_tiler/io/rasterio.py#L368-L380 def _part_read(src_path: str, *args, **kwargs) -> ImageData: with Reader(src_path) as src: @@ -233,9 +406,9 @@ def _part_read(src_path: str, *args, **kwargs) -> ImageData: # for ii, b in enumerate(img.count): # h_counts, h_keys = numpy.histogram(data[b].compressed()) # hist[f"b{ii + 1}"] = [h_counts.tolist(), h_keys.tolist()] - return xr.DataArray(data) + return xr.DataArray(data) """ - def update_selected_data(self): + """ def update_selected_data(self): layers = self.layers # TODO(aimee): if geometry hasn't changed and a previously selected layer is still selected, don't re-fetch it. self.selected_data = [] @@ -255,7 +428,7 @@ def update_selected_data(self): else: self.loading_widget_layer.open_popup() - for idx, layer in enumerate(visible_layers): + for layer in visible_layers: layer_url = layer.url ds = None title = layer.name.replace('_', ' ').upper() @@ -269,7 +442,7 @@ def update_selected_data(self): ds = xds.sel(x=slice(bounds[0], bounds[2]), y=slice(bounds[3], bounds[1])) else: uuid_pattern = r'([a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12})' - match = re.search(f"({self.titiler_endpoint}/mosaics/{uuid_pattern})/tiles", layer_url) + match = re.search(f"({titiler_endpoint}/mosaics/{uuid_pattern})/tiles", layer_url) if match: mosaic_url = match.groups()[0] # From titiler docs http://titiler.maap-project.org/docs @@ -278,35 +451,37 @@ def update_selected_data(self): assets_endpoint = f"{self.titiler_stac_endpoint}/mosaicjson/{str_bounds}/assets?url={mosaic_url}/mosaicjson" # create a dataset from multiple COGs assets_response = requests.get(assets_endpoint) - datasets = [] - assets = assets_response.json() - ds = self.gen_mosaic_dataset_reader(assets, bounds) + if assets_response.status_code == 200: + assets = assets_response.json() + ds = self.gen_mosaic_dataset_reader(assets, bounds) if ds.any(): ds.attrs["title"] = title self.selected_data.append(ds) - return self.selected_data + return self.selected_data """ - def error_message(self, msg): - out = ipywidgets.Output() + """ def error_message(self, msg): + out = Output() with out: print(msg) self.gen_popup_icon(msg) display() - return + return """ # TODO(aimee): if you try and create a histogram for more than one layer, it creates duplicates in the popup - def create_histograms(self, b): + """ def create_histograms(self, b): + print(self, b) if self.histogram_layer in self.layers: self.remove_layer(self.histogram_layer) # TODO(aimee): make this configurable minx, maxx = [0, 500] plot_args = {"range": (minx, maxx)} fig = plt.figure() - hist_widget = ipywidgets.VBox() + hist_widget = VBox() try: self.update_selected_data() except Exception as e: return self.error_message(e) + if len(self.selected_data) == 0: return self.error_message("No data or bounding box selected.") else: @@ -314,7 +489,7 @@ def create_histograms(self, b): axes = fig.add_subplot(int(f"22{idx+1}")) plot_args['ax'] = axes # create a histogram - out = ipywidgets.Output() + out = Output() with out: out.clear_output() try: @@ -332,17 +507,12 @@ def create_histograms(self, b): self.histogram_layer = histogram_layer self.remove_layer(self.loading_widget_layer) self.add_layer(histogram_layer) - return None - - def draw_biomass_map(self): - self.add_biomass_layers() - self.add_toolbar() - return None - + return None """ + # generates warning/error popup - def gen_popup_icon(self, msg): + """ def gen_popup_icon(self, msg): warning_msg = HTML() warning_msg.value=f"{msg}" popup_warning = Popup(location=self.bbox_centroid or self.center, draggable=True, child=warning_msg) self.warning_layer=popup_warning - self.add_layer(popup_warning); + self.add_layer(popup_warning) """ diff --git a/stac_ipyleaflet/data/biomass-layers.csv b/stac_ipyleaflet/data/biomass-layers.csv index 6b61b22..1272038 100644 --- a/stac_ipyleaflet/data/biomass-layers.csv +++ b/stac_ipyleaflet/data/biomass-layers.csv @@ -9,6 +9,6 @@ GEDI L4B,"https://titiler.dit.maap-project.org/cog/tiles/{z}/{x}/{y}.png?url=s3: GEDI L4B SE,"https://titiler.dit.maap-project.org/cog/tiles/{z}/{x}/{y}.png?url=s3://ornl-cumulus-prod-protected/gedi/GEDI_L4B_Gridded_Biomass/data/GEDI04_B_MW019MW138_02_002_05_R01000M_SE.tif&rescale=0,310&colormap_name=reds" CCI Biomass,"https://titiler.maap-project.org/mosaics/f343756d-bf15-4095-a8f3-f4fcbb26b5f9/tiles/{z}/{x}/{y}?rescale=0,400&bidx=1&colormap_name=gist_earth_r" CCI Biomass SD,"https://titiler.maap-project.org/mosaics/f343756d-bf15-4095-a8f3-f4fcbb26b5f9/tiles/{z}/{x}/{y}?rescale=0,500&bidx=2&colormap_name=reds" -Paraguay Estimated Biomass,"https://titiler.maap-project.org/cog/tiles/{z}/{x}/{y}.png?url=s3://maap-landing-zone-gccops/user-added/uploaded_objects/0bfec58c-45fb-464e-b301-b1afbdf5249e/5_Biomass_cog.masked.tif&nodata=0&bidx=1&rescale=0,400&colormap_name=gist_earth_r" +Paraguay Estimated Biomass,"https://titiler.maap-project.org/cog/tiles/{z}/{x}/{y}.png?url=s3://maap-landing-zone-gccops/user-added/uploaded_objects/0bfec58c-45fb-464e-b301-b1afbdf5249e/5_biomass_cog.masked.tif&nodata=0&bidx=1&rescale=0,400&colormap_name=gist_earth_r" Paraguay Forest Mask,"https://titiler.maap-project.org/cog/tiles/{z}/{x}/{y}.png?url=s3://maap-landing-zone-gccops/user-added/uploaded_objects/45fe2e6f-2007-4cb1-964a-f337f39f4fdc/1_forest_cog.masked.tif&rescale=0,1&nodata=0&colormap_name=greens" Paraguay Tree Cover,"https://titiler.maap-project.org/cog/tiles/{z}/{x}/{y}.png?url=s3://maap-landing-zone-gccops/user-added/uploaded_objects/ee5eb60c-3c01-4789-ae8e-c03f1d719440/4_tree_cover_cog.masked.tif&rescale=0,75&nodata=0&colormap_name=greens" diff --git a/stac_ipyleaflet/stac_discovery/stac.py b/stac_ipyleaflet/stac_discovery/stac.py index 327b1f2..e396d71 100644 --- a/stac_ipyleaflet/stac_discovery/stac.py +++ b/stac_ipyleaflet/stac_discovery/stac.py @@ -1,38 +1,74 @@ -# Code taken from https://github.com/giswqs/leafmap/blob/master/leafmap/stac.py - +# [SOME] code taken from https://github.com/giswqs/leafmap/blob/master/leafmap/stac.py from pystac_client import ItemSearch -import os -import pystac import requests class Stac(): - def stac_bands(url=None, collection=None, item=None, titiler_stac_endpoint=None, **kwargs): - """Get band names of a single SpatialTemporal Asset Catalog (STAC) item. + def organize_collections(collections=[]): + output_collections = [] + # print(type(collections), collections) + for collection in collections: + try: + data = collection.to_dict() + # print(data) + collection_obj = {} + collection_obj["id"] = data["id"].strip() + collection_obj["title"] = data["title"].strip() + + start_date = data["extent"]["temporal"]["interval"][0][0] + end_date = data["extent"]["temporal"]["interval"][0][1] + + if start_date is not None: + collection_obj["start_date"] = start_date.split("T")[0] + else: + collection_obj["start_date"] = "" + + if end_date is not None: + collection_obj["end_date"] = end_date.split("T")[0] + else: + collection_obj["end_date"] = "" + collection_obj["bbox"] = ", ".join( + [str(coord) for coord in data["extent"]["spatial"]["bbox"][0]] + ) + + for l in data["links"]: + if l["rel"] == "about": + collection_obj["metadata"] = l["href"] + if l["rel"] == "self": + collection_obj["href"] = l["href"] + + collection_obj["description"] = ( + data["description"] + .replace("\n", " ") + .replace("\r", " ") + .replace("\\u", " ") + .replace(" ", " ") + ) + collection_obj["license"] = data["license"] + output_collections.append(collection_obj) + except Exception as err: + print("Error: ", collection) + print(err) + return None + if len(output_collections) > 0: + output_collections.sort(key= lambda x:x['title']) + return output_collections + + def get_item_info(url=None, **kwargs): + """Get INFO of a single SpatialTemporal Asset Catalog (STAC) **COG** item. Args: url (str): HTTP URL to a STAC item - collection (str): STAC collection ID, e.g., landsat-8-c2-l2. - item (str): STAC item ID, e.g., LC08_L2SP_047027_20201204_02_T1. - titiler_stac_endpoint (str, optional): Titiler endpoint. Defaults to None. Returns: - list: A list of band names + json: Response with Item info. """ - if url is None and collection is None: - raise ValueError("Either url or collection must be specified. stac_bands") - - if url is not None: - kwargs["url"] = url - if collection is not None: - kwargs["collection"] = collection - if item is not None: - kwargs["item"] = item - - if isinstance(titiler_stac_endpoint, str): - r = requests.get(f"{titiler_stac_endpoint}/stac/assets", params=kwargs).json() - else: - r = requests.get(titiler_stac_endpoint.url_for_stac_assets(), params=kwargs).json() + if url is None: + raise ValueError("Item url must be specified to get stac_bands") + if isinstance(url, str): + r = requests.get(f"{url}", ).json() + return r + def stac_tile( url=None, @@ -155,6 +191,7 @@ def add_stac_layer( tile_url = Stac.stac_tile( url, collection, item, assets, bands, titiler_stac_endpoint, **kwargs ) + return tile_url bounds = Stac.stac_bounds(url, collection, item, titiler_stac_endpoint) self.add_tile_layer(tile_url, name, attribution, opacity, shown) self.fit_bounds([[bounds[1], bounds[0]], [bounds[3], bounds[2]]]) @@ -233,4 +270,87 @@ def stac_search( return info else: return search + + def get_metadata( + data_type="cog", + titiler_stac_endpoint=None, + url=None, + max_size=None, + **kwargs, + ): + if url is not None: + kwargs["url"] = url + if max_size is not None: + kwargs["max_size"] = max_size + + if isinstance(titiler_stac_endpoint, str): + r = requests.get(f"{titiler_stac_endpoint}/{data_type}/metadata", params=kwargs).json() + return r + else: + return "Cannot process request: titiler stac endpoint not provided." + + def get_tile_url( + data_type="cog", + url=None, + collection=None, + item=None, + assets=None, + bands=None, + palette=None, + titiler_stac_endpoint=None, + **kwargs, + ): + """Get a tile layer url from a single SpatialTemporal Asset Catalog (STAC) item. + Args: + url (str): HTTP URL to a STAC item + collection (str): STAC collection ID, e.g., landsat-8-c2-l2. + item (str): STAC item ID, e.g., LC08_L2SP_047027_20201204_02_T1. + assets (str | list): STAC asset ID, e.g., ["SR_B7", "SR_B5", "SR_B4"]. + bands (list): A list of band names, e.g., ["SR_B7", "SR_B5", "SR_B4"] + titiler_stac_endpoint (str, optional): Titiler endpoint, Defaults to None. + Returns: + str: Returns the STAC Tile layer URL. + """ + if url is None and collection is None: + raise ValueError("Either url or collection must be specified. stac_tile") + + kwargs["rescale"] = "0,50" + + if url is not None: + kwargs["url"] = url + if collection is not None: + kwargs["collection"] = collection + if item is not None: + kwargs["item"] = item + + if palette is not None: + # kwargs["colormap_name"] = kwargs["palette"].lower() + kwargs["colormap_name"] = palette + # del kwargs["palette"] + + if isinstance(bands, list) and len(set(bands)) == 1: + bands = bands[0] + + if isinstance(assets, list) and len(set(assets)) == 1: + assets = assets[0] + + if isinstance(bands, str): + bands = bands.split(",") + if isinstance(assets, str): + assets = assets.split(",") + + kwargs["assets"] = assets + TileMatrixSetId = "WebMercatorQuad" + if "TileMatrixSetId" in kwargs.keys(): + TileMatrixSetId = kwargs["TileMatrixSetId"] + kwargs.pop("TileMatrixSetId") + + if isinstance(titiler_stac_endpoint, str): + r = requests.get( + f"{titiler_stac_endpoint}/{data_type}/{TileMatrixSetId}/tilejson.json", + params=kwargs, + ).json() + return r + else: + return "STAC ENDPOINT IS NECESSARY." \ No newline at end of file diff --git a/stac_ipyleaflet/stac_discovery/stac_widget.py b/stac_ipyleaflet/stac_discovery/stac_widget.py index d232c89..e140b6f 100644 --- a/stac_ipyleaflet/stac_discovery/stac_widget.py +++ b/stac_ipyleaflet/stac_discovery/stac_widget.py @@ -1,151 +1,278 @@ -from ipywidgets import VBox, Output, Layout, Text, ToggleButtons, Dropdown, DatePicker, HBox, Textarea, Checkbox -import pandas as pd -from importlib.resources import files from datetime import datetime -from pathlib import Path -from typing import Any -from .stac import Stac +from ipywidgets import Box, Combobox, DatePicker, Dropdown, HBox, HTML +from ipywidgets import Layout, Output, RadioButtons, Tab, ToggleButtons, VBox +from pystac_client import Client +from stac_ipyleaflet.stac_discovery.stac import Stac class StacDiscoveryWidget(): - def template(self) -> VBox(): - standard_width = "400px" - padding = "0px 0px 0px 5px" - style = {"description_width": "initial"} + def template(self) -> Box( style={"max_height: 200px"}): + titiler_stac_endpoint = "https://titiler.maap-project.org" + standard_width = "440px" + styles = { + "init": {"description_width": "initial",}, + "desc": "white-space:normal;font-size:smaller; max-height:80px;" + } + layouts = { + "default": Layout(width=standard_width, padding="2px 6px"), + "header": Layout(width=standard_width, padding="2px 6px", margin="2px 2px -6px 2px"), + "buttons": Layout(display="flex", flex_flow="row", justify_content="flex-end", margin="0.5rem 1.5rem"), + "radio": Layout(display="flex", width="max-content", padding="2px 6px"), + } + output = Output( - layout=Layout(width=standard_width, padding=padding, overflow="auto") + layout=Layout(width=standard_width, height="200px", padding="4px 8px 4px 8px", overflow="auto") ) - stac_data = [] # Templates for the STAC Discovery Widget stac_widget = VBox() - padding = "0px 0px 0px 5px" - style = {"description_width": "initial"} - - nasa_cmr_path = files("stac_ipyleaflet") / "stac_discovery" / "catalogs" / "nasa_maap_stac.tsv" - stac_info = { - "MAAP STAC": { - "filename": nasa_cmr_path, - "name": "id", - "url": "href", - "description": "title", - } + stac_widget.layout.width="480px" + stac_widget.layout.height="400px" + stac_widget.layout.flex_flow="column" + stac_widget.layout.overflow="auto" + + stac_catalogs = [ + {"name": "MAAP STAC", "url": "https://stac.maap-project.org"}, + # {"name": "VEDA STAC", "url": "https://staging-stac.delta-backend.com"}, + # {"name": "MAAP STAC", "url": "https://wssn144yw1.execute-api.us-west-2.amazonaws.com/"}, + {"name": "Element84 Earth Search", "url": "https://earth-search.aws.element84.com/v1"}, + # {"name": "Microsoft Planetary Computer", "url": "https://planetarycomputer.microsoft.com/api/stac/v1"}, + ] + # make list of name values from stac_catalogs + catalog_options = sorted([c['name'] for c in stac_catalogs]) + selected_catalog = stac_catalogs[0] + for cat in stac_catalogs: + # print(cat) + stac_client = Client.open(cat["url"], headers=[]) + collections_object = stac_client.get_all_collections() + collections = Stac.organize_collections(collections_object) + cat["collections"] = collections + if "collections" not in selected_catalog: + print("COLLECTIONS NOT FOUND") + return + # else: + selected_collection_options = sorted([c for c in selected_catalog["collections"]], key=lambda c: c["id"]) + selected_collection = selected_collection_options[0] + self.stac_data = { + "catalog": selected_catalog, + "collection": selected_collection, + "items": [], + "layer_added": False } - connections = list(stac_info.keys()) - - # Template - catalogs = Dropdown( - options=connections, - value="MAAP STAC", - description="Catalog:", - style=style, - layout=Layout(width="450px", padding=padding), + # STAC Widget Items + catalogs_dropdown = Dropdown( + options=catalog_options, + value=self.stac_data["catalog"]["name"], + style=styles["init"], + disabled=True, + layout=layouts["default"], ) - - df = pd.read_csv(stac_info[catalogs.value]["filename"], sep="\t") - datasets = df[stac_info[catalogs.value]["name"]].tolist() - default = df.iloc[0] # values mapped to .tsv - - collection = Dropdown( - options=datasets, - value=default["id"], - description="Collection:", - style=style, - layout=Layout(width="450px", padding=padding), + catalogs_box = VBox( + [ + HTML(value="Catalog", style=styles["init"], layout=layouts["header"],), + catalogs_dropdown + ] + ) + collections_dropdown = Dropdown( + options=[c["id"] for c in selected_collection_options], + value=self.stac_data["collection"]["id"], + style=styles["init"], + layout=layouts["default"], + ) + collections_box = VBox( + [ + HTML(value="Collection", style=styles["init"], layout=layouts["header"],), + collections_dropdown + ] ) - collection_description = Text( - value=default["description"], - description="Description:", - style=style, - layout=Layout(width="450px", padding=padding), + collection_description = HTML( + value=f'
{self.stac_data["collection"]["description"]}
', + style=styles["init"], + layout=layouts["default"], ) - collection_url = Text( - value=default["href"], - description="URL:", - tooltip="STAC Catalog URL", - style=style, - layout=Layout(width="450px", padding=padding), + collection_description_box = VBox( + [ + HTML(value="Description", style=styles["init"], layout=layouts["header"],), + collection_description + ] ) - start_date = DatePicker( - value=datetime.strptime(default["start_date"], "%Y-%m-%d"), - description="Start:", - disabled=False if collection.value else True, - style=style, - layout=Layout(width="215px", padding=padding), + collection_url = HTML( + value=f'{self.stac_data["collection"]["href"]}', + style=styles["init"], + layout=layouts["default"], ) - end_date = DatePicker( - value=datetime.strptime(default["end_date"], "%Y-%m-%d"), - description="End:", - disabled=False if collection.value else True, - style=style, - layout=Layout(width="215px", padding=padding), + stac_browser_url = self.stac_data["collection"]["href"].replace("https://", "https://stac-browser.maap-project.org/external/") + collection_url_browser = HTML( + value=f'View in STAC Browser', + style=styles["init"], + layout=layouts["default"], ) - items = Dropdown( + collection_url.style.text_color = "blue" + collection_url_browser.style.text_color = "blue" + collection_url_box = VBox( + [ + HTML(value="URL", style=styles["init"], layout=layouts["header"],), + collection_url, collection_url_browser + ] + ) + collection_start_date = DatePicker( + value=datetime.strptime(self.stac_data["collection"]["start_date"], "%Y-%m-%d"), + description="Start", + disabled=False if collections_dropdown.value else True, + style=styles["init"], + layout=layouts["default"], + ) + collection_end_date = DatePicker( + value=datetime.strptime(self.stac_data["collection"]["end_date"], "%Y-%m-%d"), + description="End", + disabled=False if collections_dropdown.value else True, + style=styles["init"], + layout=layouts["default"], + ) + collection_dates_box = VBox( + [ + HTML(value="Date Range", style=styles["init"], layout=layouts["header"],), + HBox([collection_start_date, collection_end_date]) + ] + ) + defaultItemsDropdownText = "Select an Item" + items_dropdown = Dropdown( options=[], - description="Item:", - style=style, - layout=Layout(width="450px", padding=padding), + value=None, + style=styles["init"], + layout=layouts["default"] ) - layer_name = Text( - value="STAC Layer", - description="Layer name:", - tooltip="Enter a layer name for the selected file", - style=style, - layout=Layout(width="454px", padding=padding), + items_box = VBox( + [ + HTML(value="Items", style=styles["init"], layout=layouts["header"],), + items_dropdown + ] ) + # layer_name = Text( + # value="STAC Layer", + # description="Layer name:", + # tooltip="Enter a layer name for the selected file", + # style=styles["init"], + # layout=layouts["default"], + # ) band_width = "125px" - singular_band = Dropdown( - description="Band:", + + singular_band_dropdown = Dropdown( + options=[1], tooltip="Present Band", - style=style, - layout=Layout(width=band_width, padding=padding), + style=styles["init"], + layout=layouts["default"], + ) + + singular_band_dropdown_box = VBox( + [ + HTML(value="Band(s)", style=styles["init"], layout=layouts["header"],), + singular_band_dropdown + ] ) - vmin = Text( + vmin = HTML( value=None, description="vmin:", tooltip="Minimum value of the raster to visualize", - style=style, - layout=Layout(width="148px", padding=padding), + style=styles["init"], + layout=Layout(width=band_width, padding="4px 8px"), ) - vmax = Text( + vmax = HTML( value=None, description="vmax:", tooltip="Maximum value of the raster to visualize", - style=style, - layout=Layout(width="148px", padding=padding), + style=styles["init"], + layout=Layout(width=band_width, padding="4px 8px"), ) - nodata = Text( + nodata = HTML( value=None, description="Nodata:", tooltip="Nodata the raster to visualize", - style=style, - layout=Layout(width="150px", padding=padding), + style=styles["init"], + layout=Layout(width=band_width, padding="4px 8px"), ) - def list_palettes(add_extra=False, lowercase=False): + cmaps = [('Perceptually Uniform Sequential', [ + 'viridis', 'plasma', 'inferno', 'magma', 'cividis']), + ('Sequential', [ + 'Greys', 'Purples', 'Blues', 'Greens', 'Oranges', 'Reds', + 'YlOrBr', 'YlOrRd', 'OrRd', 'PuRd', 'RdPu', 'BuPu', + 'GnBu', 'PuBu', 'YlGnBu', 'PuBuGn', 'BuGn', 'YlGn']), + ('Sequential (2)', [ + 'binary', 'gist_yarg', 'gist_gray', 'gray', 'bone', 'pink', + 'spring', 'summer', 'autumn', 'winter', 'cool', 'Wistia', + 'hot', 'afmhot', 'gist_heat', 'copper']), + ('Diverging', [ + 'PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu', + 'RdYlBu', 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic']), + ('Cyclic', ['twilight', 'twilight_shifted', 'hsv']), + ('Qualitative', [ + 'Pastel1', 'Pastel2', 'Paired', 'Accent', + 'Dark2', 'Set1', 'Set2', 'Set3', + 'tab10', 'tab20', 'tab20b', 'tab20c']), + ('Miscellaneous', [ + 'flag', 'prism', 'ocean', 'gist_earth', 'terrain', 'gist_stern', + 'gnuplot', 'gnuplot2', 'CMRmap', 'cubehelix', 'brg', + 'gist_rainbow', 'rainbow', 'jet', 'turbo', 'nipy_spectral', + 'gist_ncar'])] + + def list_palettes(add_extra=False, lowercase=False, category=""): """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 - - palette_options = list_palettes(lowercase=True) - palette = Dropdown( + if not category == "": + all_colormap_options = plt.colormaps() + filtered_color_options = list(filter(lambda x: x[0].startswith(category), cmaps)) + palette_options = list(map(lambda x: x[1], filtered_color_options))[0] + if add_extra: + palette_options += ["dem", "ndvi", "ndwi"] + if lowercase: + palette_options = [i.lower() for i in palette_options] + palette_options.sort() + return palette_options + + def list_palette_categories(): + palette_categories = list(map(lambda x: x[0], cmaps)) + return palette_categories + + + palette_category_options = list_palette_categories() + palette_categories_dropdown = Dropdown( + options=palette_category_options, + value=palette_category_options[0], + layout=layouts["default"], + style=styles["init"], + ) + palette_categories_dropdown_box = VBox( + [ + HTML(value="Palette Category", style=styles["init"], layout=layouts["header"],), + palette_categories_dropdown + ] + ) + palette_options = list_palettes(lowercase=True, category=palette_categories_dropdown.value) + # palettes_dropdown = Dropdown( + # options=palette_options, + # value=palette_options[0], + # description="Palette:", + # layout=layouts["default"], + # style=styles["init"], + # ) + palette_radiobuttons = RadioButtons( options=palette_options, - value=None, - description="palette:", - layout=Layout(width="300px", padding=padding), - style=style, + value=palette_options[0], + layout=layouts["radio"], + style=styles["init"], + ) + palettes_radiobuttons_box = VBox( + [ + HTML(value="Palette", style=styles["init"], layout=layouts["header"],), + palette_radiobuttons + ] ) # TODO: Add STAC layers to LayerGroup instead of base # TODO: Add LayerGroup control to utilize STAC LayerGroup @@ -154,154 +281,247 @@ def list_palettes(add_extra=False, lowercase=False): # value=False, # description="Additional params", # indent=False, - # layout=Layout(width="154px", padding=padding), - # style=style, + # layout=layouts["default"], + # style=styles["init"], # ) # add_params_text = "Additional parameters in the format of a dictionary, for example, \n {'palette': ['#006633', '#E5FFCC', '#662A00', '#D8D8D8', '#F5F5F5'], 'expression': '(SR_B5-SR_B4)/(SR_B5+SR_B4)'}" # add_params = Textarea( # value="", # placeholder=add_params_text, - # layout=Layout(width="450px", padding=padding), - # style=style, + # layout=layouts["default"], + # style=styles["init"], # ) - # params_widget = VBox() - raster_options = VBox() - buttons = ToggleButtons( + # params_widget = VBox([checkbox, add_params]) + raster_options = VBox([ + HBox([singular_band_dropdown_box]), + HBox([palette_categories_dropdown_box]), + HBox([palettes_radiobuttons_box]), + ]) + stac_buttons = ToggleButtons( value=None, - options=["Search", "Display", "Reset", "Close"], - tooltips=["Search Collection", "Display Image", "Reset Values", "Close"], - button_style="primary", - style={"button_width": "50px"}, - padding="5px 0px 0px 5px" + options=["Display "], + icons=["map"], + disabled=True, + tooltips=["Display selected Item on the Map"], ) - buttons.style.button_width ="50px" + buttons_box = Box([stac_buttons], layout=layouts["buttons"]) + stac_tab_labels = ['Catalog', 'Visualization'] + tab_widget_children = [] + stac_tab_widget = Tab() + + for label in stac_tab_labels: + tab_content = VBox() + if label == 'Catalog': + tab_content.children = [ + catalogs_box, + collections_box, + collection_description_box, + collection_url_box, + collection_dates_box, + items_box + ] + elif label == 'Visualization': + tab_content.children = [ + raster_options + ] + tab_widget_children.append(tab_content) + stac_tab_widget.children = tab_widget_children + stac_tab_widget.titles = stac_tab_labels stac_widget.children = [ - catalogs, - collection, - collection_description, - collection_url, - HBox([start_date, end_date]), - items, - raster_options, - buttons, + # catalogs_box, + # collections_box, + # collection_description_box, + # collection_url_box, + # collection_dates_box, + # items_box, + # palettes_dropdown, + # raster_options, + stac_tab_widget, + buttons_box, output ] - - # Event Watchers - def catalogs_changed(change): - if change["new"]: - df = pd.read_csv(stac_info[catalogs.value]["filename"], sep="\t") - datasets = df[stac_info[catalogs.value]["name"]].tolist() - collection.options = datasets - collection.value = datasets[0] - - catalogs.observe(catalogs_changed, names="value") - - def collection_changed(change): - if change["new"]: - output.clear_output() - df = pd.read_csv(stac_info[catalogs.value]["filename"], sep="\t") - df = df[df[stac_info[catalogs.value]["name"]] == collection.value] - collection_description.value = df[stac_info[catalogs.value]["description"]].tolist()[0] - collection_url.value = df[stac_info[catalogs.value]["url"]].tolist()[0] - - current_collection_index = df.index[df["id"] == collection.value].tolist() - start_date.value = datetime.strptime(df["start_date"][current_collection_index].values[0], "%Y-%m-%d") - end_date.value = datetime.strptime(df["end_date"][current_collection_index].values[0], "%Y-%m-%d") - items.options = [] - raster_options.children = [] - - collection.observe(collection_changed, names="value") - - def update_bands(): - assets = stac_data[0][items.value]["assets"] - bands = [x for x in assets if assets[x].media_type and ("cloud-optimized" in assets[x].media_type)] - - if len(bands) == 1: - raster_options.children = [ - HBox([singular_band]), - HBox([palette]), # checkbox - # params_widget, - ] - singular_band.options = bands - else: - raster_options.children = [] - - default_bands = Stac.set_default_bands(bands) - try: - singular_band.value = default_bands[0] - except Exception as e: - singular_band.value = None - - def items_changed(change): - if change["new"]: - output.clear_output() - layer_name.value = items.value - update_bands() - if not singular_band.options: - with output: - print("This item cannot be added as a layer. Only cloud-optimized geotiffs are supported at this time.") - vmin.value = "" - vmax.value = "" - - items.observe(items_changed, names="value") - - def reset_values(): - collection.value = default["id"] - collection_description.value = default["description"] - collection_url.value = default["href"] - start_date.value = datetime.strptime(default["start_date"], "%Y-%m-%d") - end_date.value = datetime.strptime(default["end_date"], "%Y-%m-%d") - palette.value = None - items.options = [] - raster_options.children = [] - - def button_clicked(change): - if change["new"] == "Search": + def prep_data_display_settings(): + is_displayable = False + + assets = [i for i in self.stac_data["items"] if i["id"] == items_dropdown.value][0]["assets"] + item_href = [i for i in self.stac_data["items"] if i["id"] == items_dropdown.value][0]["href"] + metadata = Stac.get_item_info(url=item_href) + if "assets" in metadata: + self.stac_data["metadata"] = metadata + + # with output: + # output.clear_output() + # print("SELECTED ITEM", [i for i in self.stac_data["items"] if i["id"] == items_dropdown.value][0]) + # print("METADATA", json.dumps(metadata)) + + for asset in assets: + data_asset = assets[asset] + self.stac_data["data_href"] = data_asset.get_absolute_href() + data_types = data_asset.media_type + # print(f"{asset} data type:", data_types) + if "application=geotiff" in data_types and "profile=cloud-optimized" in data_types: + is_displayable = True + # if "statistics" in metadata: + # minv, maxv = metadata["statistics"]["1"]["min"], metadata["statistics"]["1"]["max"] + # print("MIN/MAX", minv, maxv) + if "band_metadata" in metadata: + bands = [b for b in metadata["band_metadata"][0] if len(b) > 0] + default_bands = Stac.set_default_bands(bands) + # print("BANDS", default_bands) + if len(bands) == 1: + raster_options.children = [ + HBox([singular_band_dropdown_box]), + HBox([palette_categories_dropdown_box]), + HBox([palettes_radiobuttons_box]), + # checkbox, + # params_widget, + ] + singular_band_dropdown.options = default_bands + singular_band_dropdown.value = default_bands[0] + # stac_tab_widget.selected_index = 1 + else: + raster_options.children = [] + singular_band_dropdown.value = None + + if is_displayable: + stac_buttons.disabled = False + with output: + output.clear_output() + print("Item is ready for display.") + else: with output: output.clear_output() + print("This item cannot displayed. Only Cloud-Optimized GeoTIFFs are supported at this time.") + + def query_collection_items(selected_collection): + # print("SELECTED TO QUERY", selected_collection) + items_dropdown.options = [] + items_dropdown.value = None + with output: + output.clear_output() + print("Retrieving items...") + try: + # geometries = [self.draw_control.last_draw['geometry']] + # print(geometries) + if isinstance(collection_start_date.value, datetime): + start_date_query = collection_start_date.value.strftime("%Y-%m-%d") + else: + start_date_query = str(collection_start_date.value) + + if isinstance(collection_end_date.value, datetime): + end_date_query = collection_end_date.value.strftime("%Y-%m-%d") + else: + end_date_query = str(collection_end_date.value) + + _datetime = start_date_query + if collection_end_date.value is not None: + _datetime = _datetime + "/" + end_date_query + url = selected_collection["href"] + _query_url = url if url.endswith("/items") else url + "/items" + + print("from ",_query_url, "...") + + collection_items = Stac.stac_search( + url=_query_url, + max_items=20, + # intersects=geometries[0], + datetime=_datetime, + titiler_endpoint=self.titiler_endpoint, + get_info=True, + ) + result_items = list(collection_items.values()) + self.stac_data["items"] = result_items + items = list(collection_items.keys()) + default = [defaultItemsDropdownText] + if len(items) > 0: + options = [*default, *items] + items_dropdown.options = options + items_dropdown.value = options[0] + output.clear_output() + print(f"{len(items)} items were found - please select 1 to determine if it can be displayed.") + else: + output.clear_output() + print("No items were found within this Collection. Please select another.") - print("Retrieving items...") - try: - geometries = [self.draw_control.last_draw['geometry']] + except Exception as err: + output.clear_output() + print("COLLECTION QUERY ERROR", err) - if isinstance(start_date.value, datetime): - start_date_query = str(start_date.value.date()) - else: - start_date_query = str(start_date.value) - if isinstance(end_date.value, datetime): - end_date_query = str(end_date.value.date()) - else: - end_date_query = str(end_date.value) - _datetime = start_date_query + "/" + end_date_query + # Event Watchers + def catalogs_changed(change): + if change["new"]: + selected_catalog = [cat for cat in stac_catalogs if cat["name"] == catalogs_dropdown.value][0] + selected_collection_options = sorted([c for c in selected_catalog["collections"]], key=lambda c: c["id"]) + selected_collection = selected_collection_options[0] + collections_dropdown.options = [c["id"] for c in selected_collection_options] + collections_dropdown.value = selected_collection["id"] + self.stac_data["catalog"] = selected_catalog + self.stac_data["collection"] = selected_collection + query_collection_items(selected_collection) + + # with output: + # output.clear_output() + # print(selected_collection["id"]) - search = Stac.stac_search( - url=collection_url.value if collection_url.value.endswith("/items") else collection_url.value + "/items", - max_items=20, - intersects=geometries[0], - datetime=_datetime, - titiler_endpoint=self.titiler_endpoint, - get_info=True, - ) - stac_data.clear() - stac_data.append(search) + catalogs_dropdown.observe(catalogs_changed, names="value") + + def collection_changed(change): + if change["new"]: + selected_collection = [c for c in selected_collection_options if c["id"] == collections_dropdown.value][0] + collection_description.value = f'
{selected_collection["description"]}
' + collection_url.value = f'{selected_collection["href"]}' + stac_browser_url = selected_collection["href"].replace("https://", "https://stac-browser.maap-project.org/external/") + collection_url_browser.value = f'View in STAC Browser' + if selected_collection["start_date"] != "": + collection_start_date.value = datetime.strptime(selected_collection["start_date"], "%Y-%m-%d") + else: + collection_start_date.value = None + if selected_collection["end_date"] != "": + collection_end_date.value = datetime.strptime(selected_collection["end_date"], "%Y-%m-%d") + else: + collection_end_date.value = None + self.stac_data["collection"] = selected_collection + query_collection_items(selected_collection) + + collections_dropdown.observe(collection_changed, names="value") + + def items_changed(change): + if change["new"] and change["new"] != defaultItemsDropdownText: + prep_data_display_settings() - output.clear_output() + items_dropdown.observe(items_changed, names="value") - items.options = list(search.keys()) - items.value = list(search.keys())[0] + def palette_category_changed(change): + if change["new"]: + new_palettes = list_palettes(lowercase=True, category=palette_categories_dropdown.value) + palette_radiobuttons.options = new_palettes + palette_radiobuttons.value = new_palettes[0] + + palette_categories_dropdown.observe(palette_category_changed, names="value") + + """ def reset_values(): + selected_collection = selected_collection_options[0] + collections_dropdown.value = selected_collection["id"] + collection_description.value = f'
{selected_collection["description"]}
' + collection_url.value = f'{selected_collection["href"]}' + collection_url_browser.value = f'View in Browser' + collection_start_date.value = datetime.strptime(selected_collection["start_date"], "%Y-%m-%d") + collection_end_date.value = datetime.strptime(selected_collection["end_date"], "%Y-%m-%d") + items_dropdown.options = [] + query_collection_items(selected_collection) + # palette.value = None + # raster_options.children = [] """ - except Exception as e: - print(e) - elif change["new"] == "Display": + def button_clicked(change): + if change["new"] == "Display ": with output: output.clear_output() - if items.value: - print("Loading data...") + if not items_dropdown.value == defaultItemsDropdownText: + print(f"Loading data for {items_dropdown.value}...") # if ( # checkbox.value # and add_params.value.strip().startswith("{") @@ -310,9 +530,9 @@ def button_clicked(change): # vis_params = eval(add_params.value) # else: vis_params = {} - - if (palette.value and singular_band.options) or (palette.value and "expression" in vis_params): - vis_params["colormap_name"] = palette.value + + if (palette_radiobuttons.value and singular_band_dropdown.options) or (palette_radiobuttons.value and "expression" in vis_params): + vis_params["colormap_name"] = palette_radiobuttons.value if vmin.value and vmax.value: vis_params["rescale"] = f"{vmin.value},{vmax.value}" @@ -320,36 +540,57 @@ def button_clicked(change): if nodata.value: vis_params["nodata"] = nodata.value - if singular_band.options: - assets = singular_band.value + if singular_band_dropdown.options: + assets = singular_band_dropdown.value else: assets = "" - try: - Stac.add_stac_layer( - self, - url=stac_data[0][items.value]["href"], - collection=collection.value, - item=items.value, - assets=assets, - name=layer_name.value, - titiler_stac_endpoint=self.titiler_stac_endpoint, - **vis_params, - ) - self.stac_data = stac_data[0][items.value] - output.clear_output() - except Exception as e: - print(e) - - - elif change["new"] == "Reset": - reset_values() - - elif change["new"] == "Close": - stac_widget.layout.display = 'none' - - buttons.value = None - - buttons.observe(button_clicked, "value") + + stac_url = Stac.get_tile_url( + url=self.stac_data["data_href"], + collection=self.stac_data["collection"]["id"], + item=items_dropdown.value, + assets=assets, + palette=vis_params["colormap_name"], + titiler_stac_endpoint=titiler_stac_endpoint + ) + print("stac url:", stac_url) + if "tiles" in stac_url: + self.stac_data["tiles_url"] = stac_url["tiles"][0] + try: + if "metadata" in self.stac_data: + metadata = self.stac_data["metadata"] + if "bounds" in metadata: + bounds = self.stac_data["metadata"]["bounds"] + else: + bounds = self.stac_data["metadata"]["bbox"] + else: + bounds = [] + + tile_url = self.stac_data["tiles_url"] + if self.stac_data["layer_added"] == True: + self.layers = self.layers[:len(self.layers)-1] + self.stac_data["layer_added"] = False + self.add_tile_layer(url=tile_url, name=items_dropdown.value, attribution=items_dropdown.value) + self.stac_data["layer_added"] = True + if len(bounds) > 0: + self.fit_bounds([[bounds[1], bounds[0]], [bounds[3], bounds[2]]]) + output.clear_output() + # print("STAC URL", stac_url["tiles"][0]) + except Exception as err: + output.clear_output() + print("Display error: ", err) + + """ elif change["new"] == "Reset": + reset_values() """ + + """ elif change["new"] == "Close": + stac_widget.layout.display = 'none' """ + + stac_buttons.value = None + + stac_buttons.observe(button_clicked, "value") + + query_collection_items(selected_collection) stac_widget.layout.display = 'none' diff --git a/stac_ipyleaflet/widgets/basemaps.py b/stac_ipyleaflet/widgets/basemaps.py new file mode 100644 index 0000000..869f79c --- /dev/null +++ b/stac_ipyleaflet/widgets/basemaps.py @@ -0,0 +1,73 @@ +from ipyleaflet import basemaps, basemap_to_tiles, TileLayer +from ipywidgets import Box + +class BasemapsWidget(): + def template(self, **kwargs) -> Box(): + base_layers = [] + def make_base_layer (url=None, visible=False, name="", layer=""): + if url: + layer = TileLayer( + url=url, + name=name, + base=True, + visible=False, + **kwargs + ) + return layer + else: + layer = basemap_to_tiles(layer) + layer.base = True + layer.visible = visible + layer.name = name + return layer + + tile_layers = [ + { + "url": "https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}", + "name": "Google Terrain" + }, + { + "url": "http://services.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}", + "name": "Esri Light Gray" + }, + ] + + for tl in tile_layers: + tile_layer = make_base_layer(url=tl["url"], name=tl["name"]) + base_layers.append(tile_layer) + + basemap_layers = [ + { + "layer": basemaps.Esri.WorldImagery, + "name": "Esri World Imagery" + }, + { + "layer": basemaps.Esri.NatGeoWorldMap, + "name": "Esri National Geographic" + }, + { + "layer": basemaps.OpenStreetMap.Mapnik, + "name": "Open Street Map" + }, + { + "layer": basemaps.OpenTopoMap, + "name": "Open Topo Map" + }, + { + "layer": basemaps.Stamen.Toner, + "name": "Black & White" + }, + { + "layer": basemaps.Strava.Water, + "name": "Water" + }, + ] + + for bm in basemap_layers: + if bm["name"] == "Open Street Map": + bm_layer = make_base_layer(layer=bm["layer"], name=bm["name"], visible=True) + else: + bm_layer = make_base_layer(layer=bm["layer"], name=bm["name"]) + base_layers.append(bm_layer) + + return base_layers diff --git a/stac_ipyleaflet/widgets/draw.py b/stac_ipyleaflet/widgets/draw.py new file mode 100644 index 0000000..f90f148 --- /dev/null +++ b/stac_ipyleaflet/widgets/draw.py @@ -0,0 +1,77 @@ +from ipyleaflet import DrawControl, GeoJSON +from ipywidgets import Box, Output + +class DrawControlWidget(): + def template(self, **kwargs) -> Box( style={"max_height: 200px"} ): + main = self + bbox_out = Output() + + # Set unwanted draw controls to False or empty objects + draw_control = DrawControl( + edit=False, + remove=False, + circlemarker = {}, + polygon = {}, + polyline = {}, + ) + + aoi_coords = main.aoi_widget.children[1] + aoi_clear_button = main.aoi_widget.children[2] + + def handle_clear(self): + draw_layer = main.find_layer("draw_layer") + main.remove_layer(draw_layer) + aoi_coords.value = "Waiting for area of interest..." + aoi_clear_button.disabled = True + + def handle_draw(self, action, geo_json, **kwargs): + main.aoi_coordinates = [] + main.aoi_bbox = () + + + if action == "created": + if geo_json["geometry"]: + geojson_layer = GeoJSON( + name="draw_layer", + data=geo_json, + style={ + "fillColor": "transparent", + "color": "#333", + "weight": 3 + } + ) + main.add_layer(geojson_layer) + raw_coordinates = geo_json["geometry"]["coordinates"][0] + def bounding_box(points): + x_coordinates, y_coordinates = zip(*points) + return (min(x_coordinates), min(y_coordinates), max(x_coordinates), max(y_coordinates)) + bbox = bounding_box(raw_coordinates) + main.aoi_coordinates = raw_coordinates + main.aoi_bbox = bbox + coords_list = [coord for coord in raw_coordinates] + coords = (",
".join(map(str, coords_list))) + aoi_coords.value = f"

Coordinates:

{coords}

BBox:

{bbox}" + self.clear() + aoi_clear_button.disabled = False + aoi_clear_button.on_click(handle_clear) + + return + + def value_changed(change): + print("CHANGE", change) + + draw_control.on_draw(callback=handle_draw) + draw_control.observe(value_changed, names=["value"]) + + # Add rectangle draw control for bounding box + draw_control.rectangle = { + "shapeOptions": { + "fillColor": "transparent", + "color": "#333", + "fillOpacity": 1.0 + }, + "repeatMode": False + } + draw_control.output = bbox_out + + return draw_control