solara icon indicating copy to clipboard operation
solara copied to clipboard

linking solara.Button() callback function to method of geemap.Map() instance

Open gregorhd opened this issue 1 year ago • 3 comments

Hi there,

I'm trying to write a simple solara + geemap app where two solara.Button() elements, embedded in a separate row of a solara.Column() , cause the map, embedded in the row below the buttons, to be centered on two different coordinates when pressed.

import os
import ee
import geemap

import solara

# Get the Earth Engine token from the environment variable
earthengine_token = os.getenv('EARTHENGINE_TOKEN')

# Authenticate and initialize Earth Engine
if earthengine_token:
    credentials = ee.ServiceAccountCredentials(None, key_data=earthengine_token)
    ee.Initialize(credentials)
else:
    ee.Authenticate(auth_mode='localhost')
    ee.Initialize()

# Set zoom and coordinates
zoom = 15

# create a FeatureCollection of the two points
point1 = ee.Geometry.Point([28.902667, -2.633444])
point2 = ee.Geometry.Point([28.940827, -2.687268])

point1_feature = ee.Feature(point1, {'name': 'Point 1'})
point2_feature = ee.Feature(point2, {'name': 'Point 2'})

points = ee.FeatureCollection([point1_feature, point2_feature])

class Map(geemap.Map):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.add_data()

    def add_data(self):

        # Add points II and III points to the map
        self.addLayer(points, {'color': 'black'}, "points II and III")
        self.add_labels(
            points,
            "name",
            font_size="10pt",
            font_color="black",
            font_family="arial",
            font_weight="bold",
        )

        # Center the map on Point 2 on load
        self.centerObject(ee.Feature(points.toList(points.size()).get(1)), zoom)

    def on_button1_clicked(self, *args):
        print("Button 1 clicked")
        self.centerObject(ee.Feature(points.toList(points.size()).get(0)), zoom)

    def on_button2_clicked(self, *args):
        print("Button 2 clicked")
        self.centerObject(ee.Feature(points.toList(points.size()).get(1)), zoom)

# not sure how else to have the buttons be able to access the class methods on_button1_clicked and on_button2_clicked, respectively
map_instance = Map()

@solara.component
def Page():

    with solara.Column():
        with solara.Columns([0,0,1]):
            # call the class methods directly when clicking
            solara.Button(label="Point 1", color='green', on_click=map_instance.on_button1_clicked)
            solara.Button(label="Point 2", on_click=map_instance.on_button2_clicked)
            solara.Markdown("""
            <div style="text-align: right;">
                                ... some HTML...
            </div>
            """)
        map_instance.element(height="600px")

The UI elements all show up, and the print() calls are shown in the terminal, but the map doesn't center on the respectively other point.

solara 1.33.0 pyhd8ed1ab_0 conda-forge solara-assets 1.33.0 pyhff2d567_0 conda-forge solara-server 1.33.0 pyhff2d567_0 conda-forge solara-ui 1.33.0 pyhd8ed1ab_0 conda-forge geemap 0.32.1 pyhd8ed1ab_0 conda-forge

I'm testing this locally, but this will be running on a HuggingFace Docker image, similar to this example. Locally, I can get this to work with vanilla ipywidgets without solara but those don't seem to work when deployed on HF in combination with solara (or I'm not sure how to display an ipywidgets.widgets.VBox() as an element in the Page() function.

Something must be wrong in how I'm instantiating the Map.element() object or that it's maybe not quite the same as a Map() object as hinted at here.

Any hints would be much appreciated.

gregorhd avatar Jul 22 '24 12:07 gregorhd

You can do it with just ipywidgets. See this example: https://github.com/opengeos/surface-water-app

giswqs avatar Jul 24 '24 23:07 giswqs

Hey @gregorhd!

@giswqs proposes a very good solution, where you can work completely within the widget paradigm.

How to do this in Solara is a very good question; in the solara documentation page concerning ipywidgets libraries, as I think you noticed, we say

The map element object does not have an add_layer method. That is the downside of using the React-like API of Solara. We cannot call methods on the widget anymore.

So unfortunately I have to say doing this using the widget methods is not possible[^1].

How I would approach the problem you proposed (centering the map on different points) in Solara would be by using the center (and possibly zoom) arguments to Map. So I'd store (or get) the coordinates of all points into a list and then do something like:

active_point_index = solara.reactive(0)

def Page():
    with solara.Column():
        with solara.Columns([0,0,1]):
            ...
        Map.element(center=points[active_point_index.value])

And use the buttons to set active_point_index

[^1]: Giving this some more thought, I guess you could do something with solara.use_effect and solara.get_widget. See for example the input_date component's use_close_menu-hook. But doing this can get quite complicated.

iisakkirotko avatar Jul 25 '24 08:07 iisakkirotko

Thank you both for the helpful feedback. I did end up going back to vanilla ipywidgets, as part of the Map() class, as I couldn't get the trick with solara.reactive(0) to work.

@iisakkirotko I'll give the approach hinted at in your footnote a longer look as well, though it might be beyond me.

Nevertheless, the two projects really gel well together otherwise. Keep up the good work!

gregorhd avatar Jul 25 '24 18:07 gregorhd