openforis / sepal_ui

wrapper for ipyvuetify widgets to unify the display of voila dashboards in the SEPAL plateform
https://map-app.up.railway.app
MIT License
13 stars 4 forks source link

display a loading statebar when a layer is loading in the map #345

Closed 12rambau closed 2 years ago

12rambau commented 2 years ago

issue

I experienced some issues with the FCDM module as the map is super slow to load (it uses a adaptative buffer, the more you zoom the slower it gets it's a nightmare). The consequence is that the user clicks frantically on the zoom unzoom btn and the layer never fully loads and they eventually break the app.

I've looked into the loading widgets that exists in ipyleaflet and that's not very elegant (from my perspective).

current implementation

I came up with the idea of overwriting the add_layer method to add a StateBar at the top left corner for every layer and dynamically show them when the tile are reloading. overwritting the add_layer method is super versatile as it's the basic function called by all the others (add_ee_layer, addLayer, add_raster_layer ... etc).

questions

demo

Here is a gif demo and the the code I used.

feature_demo

from sepal_ui.mapping import SepalMap
from sepal_ui import sepalwidgets as sw 
from ipyleaflet import WidgetControl
import ee

ee.Initialize()

# create a custom statebar
class StateBar(sw.StateBar):

    def __init__(self, **kwargs):

        name = kwargs.pop("layer", "layer")
        kwargs["_metadata"] = {"layer": name} 

        super().__init__(**kwargs)

        self.msg = f"Loading {name}"
        self.loading = True

    def activate(self, change):

        if change['new']:
            self.show()
        else:
            self.hide()

        return

# define a custom Map class
class Map(SepalMap):

    layer_state_list = []

    def add_layer(self, l):

        # call the original function
        super().add_layer(l)

        # add a layer state object 
        state = StateBar(layer=l.name)
        self.layer_state_list += [state]
        self.add_control(WidgetControl(widget=state, position='topleft'))

        # link it to the layer state
        #layer = next(l for l in self.layers if l.name == name)
        l.observe(state.activate, "loading")

        return

# create the map and zoom on congo
test_map = Map()
test_map.zoom = 10
test_map.center = [5.703447982149503, 28.32275390625] 

# load hansen & al ddataset
dataset = ee.Image('UMD/hansen/global_forest_change_2020_v1_8')
treeCoverVisParam = {
  "bands": ['treecover2000'],
  "min": 0,
  "max": 100,
  "palette": ['black', 'green']
}
test_map.addLayer(dataset, treeCoverVisParam, 'tree cover')

# load the S2 RGB product (SR)
def maskS2clouds(image):

    qa = image.select('QA60');

    # Bits 10 and 11 are clouds and cirrus, respectively.
    cloudBitMask = 1 << 10;
    cirrusBitMask = 1 << 11;

    # Both flags should be set to zero, indicating clear conditions.
    mask = (
        qa
        .bitwiseAnd(cloudBitMask).eq(0)
        .And(qa.bitwiseAnd(cirrusBitMask).eq(0))
    )

    return image.updateMask(mask).divide(10000)

dataset = (
    ee.ImageCollection('COPERNICUS/S2_SR')
    .filterDate('2020-01-01', '2020-01-30')
    .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE',20))
    .map(maskS2clouds)
)

visualization = {
  "min": 0.0,
  "max": 0.3,
  "bands": ['B4', 'B3', 'B2'],
}

test_map.addLayer(dataset.mean(), visualization, 'RGB')

# display the map
test_map
dfguerrerom commented 2 years ago

Actually, that's something super useful for the end-user, so I would say that this is a good idea. Now I'm thinking on the SEPAFE app, which is adding and removing planet layers depending on the user's selections, it can add like 8 layers at the same time, so in that case, having one loading widget per image is too much... The Planet ordering module has a spinning wheel in the center of the map, what do you think about having it as native in all maps?

12rambau commented 2 years ago

the spinning wheel in the center of the map is the default behaviour of leaflet layers but they overlap when there are multiple layers (I think that's why it's set to False by default) and it could be blocking the view of the user, that's what I meant with:

I've looked into the loading widgets that exists in ipyleaflet and that's not very elegant (from my perspective).

If there is an issue with the multiple layers, we could do it another way. having one statebar wheel, the user defined if it's shown in the init params. Stating:

dfguerrerom commented 2 years ago

That's a great idea!