diff --git a/stac_ipyleaflet/core.py b/stac_ipyleaflet/core.py index a77c4c8..9d8ef92 100644 --- a/stac_ipyleaflet/core.py +++ b/stac_ipyleaflet/core.py @@ -14,10 +14,12 @@ from shapely.geometry import Polygon import xarray as xr +from stac_ipyleaflet.stac_discovery.requests import make_get_request from stac_ipyleaflet.constants import TITILER_ENDPOINT, TITILER_STAC_ENDPOINT from stac_ipyleaflet.stac_discovery.stac_widget import StacDiscoveryWidget from stac_ipyleaflet.widgets.basemaps import BasemapsWidget from stac_ipyleaflet.widgets.draw import DrawControlWidget +from stac_ipyleaflet.widgets.inspect import InspectControlWidget class StacIpyleaflet(Map): @@ -25,15 +27,11 @@ class StacIpyleaflet(Map): Stac ipyleaflet is an extension to ipyleaflet `Map`. """ - raw_input = input - - draw_control: DrawControl histogram_layer: Popup warning_layer: Popup = None loading_widget_layer: Popup = None bbox_centroid: list = [] - titiler_endpoint = TITILER_ENDPOINT titiler_stac_endpoint = TITILER_STAC_ENDPOINT def __init__(self, **kwargs): @@ -59,8 +57,12 @@ def __init__(self, **kwargs): self.selected_data = [] self.histogram_layer = None self.draw_control_added = False + self.point_control_added = False self.aoi_coordinates = [] self.aoi_bbox = () + self.applied_layers = [] + self.inspect_widget = None + self.marker_added = False gif_file = files("stac_ipyleaflet.data").joinpath("loading.gif") with open(gif_file, "rb") as f: @@ -80,16 +82,17 @@ def __init__(self, **kwargs): main_button_layout = Layout( width="120px", height="35px", border="1px solid #4682B4" ) - draw_btn = ToggleButton( - description="Draw", icon="square-o", layout=main_button_layout + # @NOTE: Break these button creations out... + interact_btn = ToggleButton( + description="Interact", icon="pencil", 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"] + interact_btn.style.text_color = self.accent_color + interact_btn.style.button_color = "transparent" + interact_btn.tooltip = "Interact with the map" + interact_btn.observe( + self.toggle_interact_widget_display, type="change", names=["value"] ) - self.buttons["draw"] = draw_btn + self.buttons["interact"] = interact_btn layers_btn = ToggleButton( description="Layers", icon="map-o", layout=main_button_layout @@ -126,12 +129,21 @@ def __init__(self, **kwargs): height="50px", ) buttons_box = HBox( - children=[draw_btn, layers_btn, stac_btn], layout=buttons_box_layout + children=[interact_btn, layers_btn, stac_btn], + layout=buttons_box_layout, ) display(buttons_box) - self.add_biomass_layers() + self.add_biomass_layers_options() self.add_custom_tools() + + self.point_control = InspectControlWidget.template( + self, + self.applied_layers, + self.interact_widget, + make_get_request, + TITILER_ENDPOINT, + ) self.draw_control = DrawControlWidget.template(self) return None @@ -142,12 +154,15 @@ def toggle_layers_widget_display(self, b): 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.interact_widget.layout.display = "none" self.buttons["stac"].value = False - self.buttons["draw"].value = False + self.buttons["interact"].value = False if self.draw_control_added: self.remove(self.draw_control) self.draw_control_added = False + if self.point_control_added: + self.remove(self.point_control) + self.point_control_added = False if not b["new"]: if self.layers_widget.layout.display == "block": self.layers_widget.layout.display = "none" @@ -157,44 +172,47 @@ def toggle_stac_widget_display(self, b): 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.interact_widget.layout.display = "none" self.buttons["layers"].value = False - self.buttons["draw"].value = False + self.buttons["interact"].value = False if self.draw_control_added: self.remove(self.draw_control) self.draw_control_added = False + if self.point_control_added: + self.remove(self.point_control) + self.point_control_added = False if not b["new"]: if self.stac_widget.layout.display == "block": self.stac_widget.layout.display = "none" - def toggle_draw_widget_display(self, b): + def toggle_interact_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 + if self.interact_widget.layout.display == "none": + self.interact_widget.layout.display = "block" self.stac_widget.layout.display = "none" self.layers_widget.layout.display = "none" self.buttons["stac"].value = False self.buttons["layers"].value = False + selected_tab = self.interact_widget.children[0].titles[ + self.interact_widget.children[0].selected_index + ] + if selected_tab == "Point": + self.add_control(self.point_control) + self.point_control_added = True + elif selected_tab == "Area": + self.add_control(self.draw_control) + self.draw_control_added = True if not b["new"]: - if self.aoi_widget.layout.display == "block": - self.aoi_widget.layout.display = "none" + if self.interact_widget.layout.display == "block": + self.interact_widget.layout.display = "none" if self.draw_control_added: self.remove(self.draw_control) self.draw_control_added = False + if self.point_control_added: + self.remove(self.point_control) + self.point_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" - + def create_aoi_tab(self): aoi_widget_desc = HTML( value="
Waiting for points of interest...
",
+ description="",
+ )
+
+ inspect_widget_button = Button(
+ description="Clear Markers",
+ tooltip="Clear Markers",
+ icon="trash",
+ disabled=True,
+ )
+
+ inspect_widget = HBox(
+ [
+ inspect_widget_desc,
+ inspect_widget_html,
+ inspect_widget_button,
+ ]
+ )
+
+ inspect_widget.layout.flex_flow = "column"
+
+ return inspect_widget
+
+ def create_interact_widget(self):
+ interact_widget = Box(style={"max-width: 420px"})
+ interact_widget.layout.flex_flow = "column"
+ interact_widget.layout.max_height = "360px"
+ interact_widget.layout.overflow = "auto"
+
+ tab_headers = ["Point", "Area"]
+ tab_children = []
+ tab_widget = Tab()
+
+ out = Output()
+ display(out)
+
+ def toggle_interact_tab_change(event):
+ selected_tab = self.interact_widget.children[0].titles[
+ event.owner.selected_index
+ ]
+ if selected_tab == "Point": # Inspect Control
+ if self.draw_control_added:
+ self.remove(self.draw_control)
+ self.draw_control_added = False
+ self.add_control(self.point_control)
+ self.point_control_added = True
+ elif selected_tab == "Area": # Draw Control
+ if self.point_control_added:
+ self.remove(self.point_control)
+ self.point_control_added = False
+ self.add_control(self.draw_control)
+ self.draw_control_added = True
+
+ for tab in tab_headers:
+ tab_content = VBox()
+ if tab == "Point":
+ hbox = self.create_inspect_tab()
+ tab_content.children = [VBox([hbox])]
+ tab_children.append(tab_content)
+ elif tab == "Area":
+ hbox = self.create_aoi_tab()
+ tab_content.children = [VBox([hbox])]
+ tab_children.append(tab_content)
+ tab_widget.children = tab_children
+ tab_widget.titles = tab_headers
+ interact_widget.children = [tab_widget]
+ interact_widget.layout.display = "none"
+ tab_widget.observe(toggle_interact_tab_change, names="selected_index")
+ return interact_widget
+
+ # @NOTE: Possibly move into its own child class file
def create_layers_widget(self):
layers_widget = Box(style={"max-width: 420px"})
layers_widget.layout.flex_flow = "column"
@@ -230,6 +324,15 @@ def create_layers_widget(self):
opacity_values = [i * 10 for i in range(10 + 1)] # [0.001, 0.002, ...]
+ def layer_checkbox_changed(change):
+ layer = next(
+ (x for x in self.layers if x.name == change.owner.description), None
+ )
+ if change.owner.value:
+ self.applied_layers.append(layer)
+ if not change.owner.value:
+ self.applied_layers.remove(layer)
+
def handle_basemap_opacity_change(change):
selected_bm = self.basemap_selection_dd.value
for l in self.layers:
@@ -239,6 +342,8 @@ def handle_basemap_opacity_change(change):
def handle_layer_opacity_change(change):
selected_layer = change.owner.description
+ if selected_layer not in self.applied_layers:
+ return
for l in self.layers:
if l.name == selected_layer:
l.opacity = change["new"]
@@ -260,11 +365,14 @@ def handle_layer_opacity_change(change):
value=layer.visible, description=layer.name, indent=False
)
jslink((layer_checkbox, "value"), (layer, "visible"))
+ layer_checkbox.observe(
+ layer_checkbox_changed, names="value", type="change"
+ )
hbox = HBox([layer_checkbox])
layer_opacity_slider = SelectionSlider(
value=1,
options=[("%g" % i, i / 100) for i in opacity_values],
- description=f"{layer.name}",
+ description=layer.name,
continuous_update=False,
orientation="horizontal",
layout=Layout(margin="-12px 0 4px 0"),
@@ -336,11 +444,11 @@ 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()
+ self.interact_widget = self.create_interact_widget()
layers_widget = VBox([self.layers_widget])
stac_widget = VBox([self.stac_widget])
- aoi_widget = VBox([self.aoi_widget])
+ interact_widget = VBox([self.interact_widget])
layers_control = WidgetControl(
widget=layers_widget, position="topright", id="layers_widget"
@@ -348,15 +456,15 @@ def add_custom_tools(self):
stack_control = WidgetControl(
widget=stac_widget, position="topright", id="stac_widget"
)
- aoi_control = WidgetControl(
- widget=aoi_widget, position="topright", id="aoi_widget"
+ interact_control = WidgetControl(
+ widget=interact_widget, position="topright", id="interact_widget"
)
self.add(layers_control)
self.add(stack_control)
- self.add(aoi_control)
+ self.add(interact_control)
- def add_biomass_layers(self):
+ def add_biomass_layers_options(self):
biomass_file = files("stac_ipyleaflet.data").joinpath("biomass-layers.csv")
with open(biomass_file, newline="") as f:
csv_reader = csv.reader(f)
@@ -413,6 +521,7 @@ def add_tile_layer(
**kwargs,
)
self.add_layer(tile_layer)
+ return tile_layer
except Exception as e:
logging.error("Failed to add the specified TileLayer.")
diff --git a/stac_ipyleaflet/stac_discovery/stac_widget.py b/stac_ipyleaflet/stac_discovery/stac_widget.py
index 1de03c6..11a9f5a 100644
--- a/stac_ipyleaflet/stac_discovery/stac_widget.py
+++ b/stac_ipyleaflet/stac_discovery/stac_widget.py
@@ -645,7 +645,7 @@ def query_collection_items(selected_collection):
max_items=20,
# intersects=geometries[0],
datetime=_datetime,
- titiler_endpoint=self.titiler_endpoint,
+ titiler_endpoint=TITILER_ENDPOINT,
get_info=True,
)
result_items = list(collection_items.values())
@@ -825,11 +825,12 @@ def button_clicked(change):
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(
+ applied_tile_layer = self.add_tile_layer(
url=tile_url,
- name=items_dropdown.value,
+ name=f'{collections_dropdown.value}, {items_dropdown.value}',
attribution=items_dropdown.value,
)
+ self.applied_layers.append(applied_tile_layer)
stac_opacity_slider.observe(
handle_stac_layer_opacity, names="value"
)
diff --git a/stac_ipyleaflet/widgets/draw.py b/stac_ipyleaflet/widgets/draw.py
index 7cad1a9..ba97b3d 100644
--- a/stac_ipyleaflet/widgets/draw.py
+++ b/stac_ipyleaflet/widgets/draw.py
@@ -1,24 +1,57 @@
from ipyleaflet import DrawControl, GeoJSON
from ipywidgets import Box, Output
+# @NOTE: This should be an extension of the IPYLEAFLET Class. Currently it is just being passed
+# in instead due to import errors
+# @TODO: Fix linting errors caused by inferred inheritance and just pass in params instead
+
+
+# @TODO: Break out shared logic between widgets into a utilities directory
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
+ # @TODO-CLEANUP: Create only one DrawControl and pass in the attributes instead
draw_control = DrawControl(
edit=False,
remove=False,
circlemarker={},
polygon={},
polyline={},
+ marker={},
)
- aoi_coords = main.aoi_widget.children[1]
- aoi_clear_button = main.aoi_widget.children[2]
+ # Add rectangle draw control for bounding box
+ draw_control.rectangle = {
+ "shapeOptions": {
+ "fillColor": "transparent",
+ "color": "#333",
+ "fillOpacity": 1.0,
+ },
+ "repeatMode": False,
+ }
+
+ tabs = {}
+
+ for i in range(2):
+ tabs[f"child{i}"] = (
+ main.interact_widget.children[0]
+ .children[i]
+ .children[0]
+ .children[0]
+ .children
+ )
+
+ point_tab_children = tabs["child0"]
+ area_tab_children = tabs["child1"]
+ aoi_coords = area_tab_children[1]
+ aoi_clear_button = area_tab_children[2]
+
+ # @TODO-CLEANUP: Duplication between tabs, pull logic out into a common utilities file
def handle_clear(self):
draw_layer = main.find_layer("draw_layer")
main.remove_layer(draw_layer)
@@ -29,6 +62,11 @@ def handle_draw(self, action, geo_json, **kwargs):
main.aoi_coordinates = []
main.aoi_bbox = ()
+ if "Coordinates" in point_tab_children[1].value:
+ area_tab_children[
+ 1
+ ].value = "Waiting for points of interest...
"
+
if action == "created":
if geo_json["geometry"]:
geojson_layer = GeoJSON(
@@ -64,21 +102,7 @@ def bounding_box(points):
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
diff --git a/stac_ipyleaflet/widgets/inspect.py b/stac_ipyleaflet/widgets/inspect.py
new file mode 100644
index 0000000..72473eb
--- /dev/null
+++ b/stac_ipyleaflet/widgets/inspect.py
@@ -0,0 +1,151 @@
+from ipyleaflet import DrawControl, MarkerCluster, Marker, DrawControl, GeoJSON
+from urllib.parse import urlparse, parse_qs
+from typing import List
+
+
+class COGRequestedData:
+ coordinates: List[float]
+ values: List[float]
+ band_names: List[str]
+
+
+class LayerData:
+ layer_name: str
+ data: COGRequestedData
+
+
+# @NOTE: This should be an extension of the IPYLEAFLET Class. Currently it is just being passed
+# in instead due to import errors
+
+
+# @TODO: Break out shared logic between widgets into a utilities directory
+class InspectControlWidget:
+ def template(
+ self, applied_layers, interact_widget, make_get_request, titiler_endpoint
+ ):
+ main = self
+
+ # @TODO-CLEANUP: Create only one DrawControl and pass in the attributes instead
+ draw_control = DrawControl(
+ edit=False,
+ remove=False,
+ circlemarker={},
+ polygon={},
+ polyline={},
+ rectangle={},
+ )
+
+ draw_control.marker = {
+ "repeatMode": False,
+ }
+
+ tabs = {}
+
+ for i in range(2):
+ tabs[f"child{i}"] = (
+ main.interact_widget.children[0]
+ .children[i]
+ .children[0]
+ .children[0]
+ .children
+ )
+
+ point_tab_children = tabs["child0"]
+ area_tab_children = tabs["child1"]
+
+ point_data = point_tab_children[1]
+ clear_button = point_tab_children[2]
+
+ def get_visible_layers_data(coordinates) -> List[LayerData]:
+ visible_layers_data = []
+ cog_partial_request_path = (
+ f"{titiler_endpoint}/cog/point/{coordinates[0]},{coordinates[1]}?url="
+ )
+ for layer in applied_layers:
+ if "/cog" in layer.url:
+ parsed_url = urlparse(layer.url)
+ parsed_query = parse_qs(parsed_url.query)
+ url = parsed_query["url"][0]
+ data = make_get_request(f"{cog_partial_request_path}{url}")
+ if data:
+ visible_layers_data.append(
+ {"layer_name": layer.name, "data": data.json()}
+ )
+ # @TODO: Add logic to grab point data for "/mosaics" layers here
+ # @NOTE: Blocked until Impact Titiler is updated to access /mosaicsjson/point
+ return visible_layers_data
+
+ def display_layer_data(layers_data: LayerData):
+ point_data.value = ""
+
+ def create_layer_data_html(layer_name, coordinates, values, band_names):
+ template = f"""
+ + + {layer_name} + +
+Waiting for points of interest...
"
+ clear_button.disabled = True
+ return
+
+ self.coordinates = []
+ if "Coordinates" in area_tab_children[1].value:
+ area_tab_children[
+ 1
+ ].value = "Waiting for area of interest...
"
+
+ if action == "created":
+ if geo_json["geometry"] and geo_json["geometry"]["type"] == "Point":
+ geojson_layer = GeoJSON(
+ name="draw_layer",
+ data=geo_json,
+ )
+ main.add_layer(geojson_layer)
+ self.coordinates = geo_json["geometry"]["coordinates"]
+
+ if len(applied_layers):
+ layers_data = get_visible_layers_data(self.coordinates)
+ if layers_data:
+ display_layer_data(layers_data)
+ else:
+ point_data.value = f"Coordinates:
{self.coordinates}
Coordinates:
{self.coordinates}