# coding=utf-8 """ Display an interactive Map for Google Earth Engine using ipyleaflet """ import ee import ipyleaflet from ipywidgets import Layout, HTML, Accordion from traitlets import * from collections import OrderedDict from .tasks import TaskManager from .assets import AssetManager from geetools import tools, utils from IPython.display import display from .tabs.layers import LayersWidget from copy import copy import traceback from .maptools import * from .widgets import ErrorAccordion from .utils import * import re ZOOM_SCALES = { 0: 156543, 1: 78271, 2: 39135, 3: 19567, 4: 9783, 5: 4891, 6: 2445, 7: 1222, 8: 611, 9: 305, 10: 152, 11: 76, 12: 38, 13: 19, 14: 9, 15: 5, 16: 2, 17: 1, 18: 0.5, 19: 0.3, 20: 0.15, 21: 0.07, 22: 0.03, } class Map(ipyleaflet.Map): tab_children_dict = Dict() EELayers = Dict() def __init__(self, tabs=('Inspector', 'Layers'), **kwargs): # Change defaults kwargs.setdefault('center', [0, 0]) kwargs.setdefault('zoom', 2) kwargs.setdefault('scroll_wheel_zoom', True) kwargs.setdefault('max_zoom', 22) super(Map, self).__init__(**kwargs) self.is_shown = False # Width and Height self.width = kwargs.get('width', None) self.height = kwargs.get('height', None) self.setDimensions(self.width, self.height) # Correct base layer name baselayer = self.layers[0] baselayer.name = 'OpenStreetMap' self.layers = (baselayer,) # Dictionary of map's handlers self.handlers = {} # Dictonary to hold tab's widgets # (tab's name:widget) self.tab_names = [] self.tab_children = [] self.tab_children_dict = OrderedDict(zip(self.tab_names, self.tab_children)) # TABS # Tab widget self.tab_widget = Tab() # Handler for Tab self.tab_widget.observe(self.handle_change_tab) self.tabs = tabs if len(tabs) > 0: # TODO: create widgets only if are in tuple # Inspector Widget (Accordion) self.inspector_wid = CustomInspector() self.inspector_wid.main.selected_index = None # this will unselect all # Task Manager Widget task_manager = TaskManager() # Asset Manager Widget asset_manager = AssetManager(self) # Layers self.layers_widget = LayersWidget(map=self) widgets = {'Inspector': self.inspector_wid, 'Layers': self.layers_widget, 'Assets': asset_manager, 'Tasks': task_manager, } handlers = {'Inspector': self.handle_inspector, 'Layers': None, 'Assets': None, 'Tasks': None, } # Add tabs and handlers for tab in tabs: if tab in widgets.keys(): widget = widgets[tab] handler = handlers[tab] self.addTab(tab, handler, widget) else: raise ValueError('Tab {} is not recognized. Choose one of {}'.format(tab, widgets.keys())) # First handler: Inspector self.on_interaction(self.handlers[tabs[0]]) # As I cannot create a Geometry with a GeoJSON string I do a workaround self.draw_types = {'Polygon': ee.Geometry.Polygon, 'Point': ee.Geometry.Point, 'LineString': ee.Geometry.LineString, } # create EELayers self.EELayers = OrderedDict() def _add_EELayer(self, name, data): """ Add a pair of name, data to EELayers. Data must be: - type: str - object: ee object - visParams: dict - layer: ipyleaflet layer """ copyEELayers = copy(self.EELayers) copyEELayers[name] = data self.EELayers = copyEELayers def _remove_EELayer(self, name): """ remove layer from EELayers """ copyEELayers = copy(self.EELayers) if name in copyEELayers: copyEELayers.pop(name) self.EELayers = copyEELayers def addBasemap(self, name, url, **kwargs): """ Add a basemap with the given URL """ layer = ipyleaflet.TileLayer(url=url, name=name, base=True, **kwargs) self.add_layer(layer) def setDimensions(self, width=None, height=None): """ Set the dimensions for the map """ def check(value, t): if value is None: return value if isinstance(value, (int, float)): return '{}px'.format(value) elif isinstance(value, (str,)): search = re.search('(\d+)', value).groups() intvalue = search[0] splitted = value.split(intvalue) units = splitted[1] if units == '%': if t == 'width': return '{}%'.format(intvalue) else: return None else: return '{}px'.format(intvalue) else: msg = 'parameter {} of setDimensions must be int or str' raise ValueError(msg.format(t)) self.layout = Layout(width=check(width, 'width'), height=check(height, 'height')) def moveLayer(self, layer_name, direction='up'): """ Move one step up a layer """ names = list(self.EELayers.keys()) values = list(self.EELayers.values()) if direction == 'up': dir = 1 elif direction == 'down': dir = -1 else: dir = 0 if layer_name in names: # if layer exists # index and value of layer to move_layer i = names.index(layer_name) condition = (i < len(names)-1) if dir == 1 else (i > 0) if condition: # if layer is not in the edge ival = values[i] # new index for layer newi = i+dir # get index and value that already exist in the new index iname_before = names[newi] ival_before = values[newi] # Change order # set layer and value in the new index names[newi] = layer_name values[newi] = ival # set replaced layer and its value in the index of moving layer names[i] = iname_before values[i] = ival_before newlayers = OrderedDict(zip(names, values)) self.EELayers = newlayers @observe('EELayers') def _ob_EELayers(self, change): new = change['new'] proxy_layers = [l for l in self.layers if l.base == True] for val in new.values(): layer = val['layer'] proxy_layers.append(layer) self.layers = tuple(proxy_layers) # UPDATE INSPECTOR # Clear options self.inspector_wid.selector.options = {} # Add layer to the Inspector Widget self.inspector_wid.selector.options = new # self.EELayers # UPDATE LAYERS WIDGET # update Layers Widget self.layers_widget.selector.options = {} self.layers_widget.selector.options = new # self.EELayers @property def addedImages(self): return sum( [1 for val in self.EELayers.values() if val['type'] == 'Image']) @property def addedGeometries(self): return sum( [1 for val in self.EELayers.values() if val['type'] == 'Geometry']) def show(self, tabs=True, layer_control=True, draw_control=False, fullscreen=True): """ Show the Map on the Notebook """ if not self.is_shown: if layer_control: # Layers Control lc = ipyleaflet.LayersControl() self.add_control(lc) if draw_control: # Draw Control dc = ipyleaflet.DrawControl(edit=False) dc.on_draw(self.handle_draw) self.add_control(dc) if fullscreen: # Control full_control = ipyleaflet.FullScreenControl() self.add_control(full_control) if tabs: display(self, self.tab_widget) else: display(self) else: if tabs: display(self, self.tab_widget) else: display(self) self.is_shown = True # Start with crosshair cursor self.default_style = {'cursor': 'crosshair'} def showTab(self, name): """ Show only a Tab Widget by calling its name. This is useful mainly in Jupyter Lab where you can see outputs in different tab_widget :param name: the name of the tab to show :type name: str """ try: widget = self.tab_children_dict[name] display(widget) except: print('Tab not found') def addImage(self, image, visParams=None, name=None, show=True, opacity=None, replace=True): """ Add an ee.Image to the Map :param image: Image to add to Map :type image: ee.Image :param visParams: visualization parameters. Can have the following arguments: bands, min, max. :type visParams: dict :param name: name for the layer :type name: str :return: the name of the added layer :rtype: str """ # Check if layer exists if name in self.EELayers.keys(): if not replace: return self.getLayer(name) else: # Get URL, attribution & vis params params = getImageTile(image, visParams, show, opacity) # Remove Layer self.removeLayer(name) else: # Get URL, attribution & vis params params = getImageTile(image, visParams, show, opacity) layer = ipyleaflet.TileLayer(url=params['url'], attribution=params['attribution'], name=name) EELayer = {'type': 'Image', 'object': image, 'visParams': params['visParams'], 'layer': layer} # self._add_EELayer(name, EELayer) # return name return EELayer def addMarker(self, marker, visParams=None, name=None, show=True, opacity=None, replace=True, inspect={'data':None, 'reducer':None, 'scale':None}): """ General method to add Geometries, Features or FeatureCollections as Markers """ if isinstance(marker, ee.Geometry): self.addGeometry(marker, visParams, name, show, opacity, replace, inspect) elif isinstance(marker, ee.Feature): self.addFeature(marker, visParams, name, show, opacity, replace, inspect) elif isinstance(marker, ee.FeatureCollection): geometry = marker.geometry() self.addGeometry(marker, visParams, name, show, opacity, replace, inspect) def addFeature(self, feature, visParams=None, name=None, show=True, opacity=None, replace=True, inspect={'data':None, 'reducer':None, 'scale':None}): """ Add a Feature to the Map :param feature: the Feature to add to Map :type feature: ee.Feature :param visParams: :type visParams: dict :param name: name for the layer :type name: str :param inspect: when adding a geometry or a feature you can pop up data from a desired layer. Params are: :data: the EEObject where to get the data from :reducer: the reducer to use :scale: the scale to reduce :type inspect: dict :return: the name of the added layer :rtype: str """ thename = name if name else 'Feature {}'.format(self.addedGeometries) # Check if layer exists if thename in self.EELayers.keys(): if not replace: print("Layer with name '{}' exists already, please choose another name".format(thename)) return else: self.removeLayer(thename) params = getGeojsonTile(feature, thename, inspect) layer = ipyleaflet.GeoJSON(data=params['geojson'], name=thename, popup=HTML(params['pop'])) self._add_EELayer(thename, {'type': 'Feature', 'object': feature, 'visParams': None, 'layer': layer}) return thename def addGeometry(self, geometry, visParams=None, name=None, show=True, opacity=None, replace=True, inspect={'data':None, 'reducer':None, 'scale':None}): """ Add a Geometry to the Map :param geometry: the Geometry to add to Map :type geometry: ee.Geometry :param visParams: :type visParams: dict :param name: name for the layer :type name: str :param inspect: when adding a geometry or a feature you can pop up data from a desired layer. Params are: :data: the EEObject where to get the data from :reducer: the reducer to use :scale: the scale to reduce :type inspect: dict :return: the name of the added layer :rtype: str """ thename = name if name else 'Geometry {}'.format(self.addedGeometries) # Check if layer exists if thename in self.EELayers.keys(): if not replace: print("Layer with name '{}' exists already, please choose another name".format(thename)) return else: self.removeLayer(thename) params = getGeojsonTile(geometry, thename, inspect) layer = ipyleaflet.GeoJSON(data=params['geojson'], name=thename, popup=HTML(params['pop'])) self._add_EELayer(thename, {'type': 'Geometry', 'object': geometry, 'visParams':None, 'layer': layer}) return thename def addFeatureLayer(self, feature, visParams=None, name=None, show=True, opacity=None, replace=True): """ Paint a Feature on the map, but the layer underneath is the actual added Feature """ visParams = visParams if visParams else {} if isinstance(feature, ee.Feature): ty = 'Feature' elif isinstance(feature, ee.FeatureCollection): ty = 'FeatureCollection' else: print('The object is not a Feature or FeatureCollection') return fill_color = visParams.get('fill_color', None) if 'outline_color' in visParams: out_color = visParams['outline_color'] elif 'border_color' in visParams: out_color = visParams['border_color'] else: out_color = 'black' outline = visParams.get('outline', 2) proxy_layer = paint(feature, out_color, fill_color, outline) thename = name if name else '{} {}'.format(ty, self.addedGeometries) img_params = {'bands':['vis-red', 'vis-green', 'vis-blue'], 'min': 0, 'max':255} # Check if layer exists if thename in self.EELayers.keys(): if not replace: print("{} with name '{}' exists already, please choose another name".format(ty, thename)) return else: # Get URL, attribution & vis params params = getImageTile(proxy_layer, img_params, show, opacity) # Remove Layer self.removeLayer(thename) else: # Get URL, attribution & vis params params = getImageTile(proxy_layer, img_params, show, opacity) layer = ipyleaflet.TileLayer(url=params['url'], attribution=params['attribution'], name=thename) self._add_EELayer(thename, {'type': ty, 'object': feature, 'visParams': visParams, 'layer': layer}) return thename def addMosaic(self, collection, visParams=None, name=None, show=False, opacity=None, replace=True): """ Add an ImageCollection to EELayer and its mosaic to the Map. When using the inspector over this layer, it will print all values from the collection """ proxy = ee.ImageCollection(collection).sort('system:time_start') mosaic = ee.Image(proxy.mosaic()) self.addImage(mosaic, visParams, name, show, opacity, replace) # modify EELayer # EELayer['type'] = 'ImageCollection' # EELayer['object'] = ee.ImageCollection(collection) # return EELayer def addImageCollection(self, collection, visParams=None, namePattern='{id}', show=False, opacity=None, datePattern='yyyyMMdd', replace=True, verbose=False): """ Add every Image of an ImageCollection to the Map :param collection: the ImageCollection :type collection: ee.ImageCollection :param visParams: visualization parameter for each image. See `addImage` :type visParams: dict :param namePattern: the name pattern (uses geetools.utils.makeName) :type namePattern: str :param show: If True, adds and shows the Image, otherwise only add it :type show: bool """ size = collection.size() collist = collection.toList(size) n = 0 while True: try: img = ee.Image(collist.get(n)) extra = dict(position=n) name = utils.makeName(img, namePattern, datePattern, extra=extra) self.addLayer(img, visParams, name.getInfo(), show, opacity, replace=replace) if verbose: print('Adding {} to the Map'.format(name)) n += 1 except ee.EEException as e: msg = 'List.get: List index must be between' if msg not in str(e): raise e break def addLayer(self, eeObject, visParams=None, name=None, show=True, opacity=None, replace=True, **kwargs): """ Adds a given EE object to the map as a layer. :param eeObject: Earth Engine object to add to map :type eeObject: ee.Image || ee.Geometry || ee.Feature :param replace: if True, if there is a layer with the same name, this replace that layer. :type replace: bool For ee.Image and ee.ImageCollection see `addImage` for ee.Geometry and ee.Feature see `addGeometry` """ if name in self.EELayers.keys(): return None visParams = visParams if visParams else {} # CASE: ee.Image if isinstance(eeObject, ee.Image): image_name = name if name else 'Image {}'.format(self.addedImages) EELayer = self.addImage(eeObject, visParams=visParams, name=image_name, show=show, opacity=opacity, replace=replace) self._add_EELayer(image_name, EELayer) # CASE: ee.Geometry elif isinstance(eeObject, ee.Geometry): geom = eeObject if isinstance(eeObject, ee.Geometry) else eeObject.geometry() kw = {'visParams':visParams, 'name':name, 'show':show, 'opacity':opacity} if kwargs.get('inspect'): kw.setdefault('inspect', kwargs.get('inspect')) self.addGeometry(geom, replace=replace, **kw) # CASE: ee.Feature & ee.FeatureCollection elif isinstance(eeObject, ee.Feature) or isinstance(eeObject, ee.FeatureCollection): feat = eeObject kw = {'visParams':visParams, 'name':name, 'show':show, 'opacity':opacity} self.addFeatureLayer(feat, replace=replace, **kw) # CASE: ee.ImageCollection elif isinstance(eeObject, ee.ImageCollection): thename = name if name else 'ImageCollection {}'.format(self.addedImages) EELayer = self.addMosaic(eeObject, visParams, thename, show, opacity, replace) self._add_EELayer(thename, EELayer) else: print("`addLayer` doesn't support adding {} objects to the map".format(type(eeObject))) def removeLayer(self, name): """ Remove a layer by its name """ if name in self.EELayers.keys(): self._remove_EELayer(name) else: print('Layer {} is not present in the map'.format(name)) return # GETTERS def getLayer(self, name): """ Get a layer by its name :param name: the name of the layer :type name: str :return: The complete EELayer which is a dict of :type: the type of the layer :object: the EE Object associated with the layer :visParams: the visualization parameters of the layer :layer: the TileLayer added to the Map (ipyleaflet.Map) :rtype: dict """ if name in self.EELayers: layer = self.EELayers[name] return layer else: print('Layer {} is not present in the map'.format(name)) return def getObject(self, name): """ Get the EE Object from a layer's name """ obj = self.getLayer(name)['object'] return obj def getVisParams(self, name): """ Get the Visualization Parameters from a layer's name """ vis = self.getLayer(name)['visParams'] return vis def getLayerURL(self, name): """ Get the layer URL by name """ layer = self.getLayer(name) tile = layer['layer'] return tile.url def getCenter(self): """ Returns the coordinates at the center of the map. No arguments. Returns: Geometry.Point :return: """ center = self.center coords = inverseCoordinates(center) return ee.Geometry.Point(coords) def getBounds(self, asGeoJSON=True): """ Returns the bounds of the current map view, as a list in the format [west, south, east, north] in degrees. Arguments: asGeoJSON (Boolean, optional): If true, returns map bounds as GeoJSON. Returns: GeoJSONGeometry|List<Number>|String """ bounds = inverseCoordinates(self.bounds) if asGeoJSON: return ee.Geometry.Rectangle(bounds) else: return bounds def centerObject(self, eeObject, zoom=None, method=1): """ Center an eeObject :param eeObject: :param zoom: :param method: experimetal methods to estimate zoom for fitting bounds Currently: 1 or 2 :type: int """ bounds = getBounds(eeObject) if bounds: try: inverse = inverseCoordinates(bounds) centroid = ee.Geometry.Polygon(inverse) \ .centroid().getInfo()['coordinates'] except: centroid = [0, 0] self.center = inverseCoordinates(centroid) if zoom: self.zoom = zoom else: self.zoom = getZoom(bounds, method) def _update_tab_children(self): """ Update Tab children from tab_children_dict """ # Set tab_widget children self.tab_widget.children = tuple(self.tab_children_dict.values()) # Set tab_widget names for i, name in enumerate(self.tab_children_dict.keys()): self.tab_widget.set_title(i, name) def addTab(self, name, handler=None, widget=None): """ Add a Tab to the Panel. The handler is for the Map :param name: name for the new tab :type name: str :param handler: handle function for the new tab. Arguments of the function are: - type: the type of the event (click, mouseover, etc..) - coordinates: coordinates where the event occurred [lon, lat] - widget: the widget inside the Tab - map: the Map instance :param widget: widget inside the Tab. Defaults to HTML('') :type widget: ipywidgets.Widget """ # Widget wid = widget if widget else HTML('') # Get tab's children as a list # tab_children = list(self.tab_widget.children) tab_children = self.tab_children_dict.values() # Get a list of tab's titles # titles = [self.tab_widget.get_title(i) for i, child in enumerate(tab_children)] titles = self.tab_children_dict.keys() # Check if tab already exists if name not in titles: ntabs = len(tab_children) # UPDATE DICTS # Add widget as a new children self.tab_children_dict[name] = wid # Set the handler for the new tab if handler: def proxy_handler(f): def wrap(**kwargs): # Add widget to handler arguments kwargs['widget'] = self.tab_children_dict[name] coords = kwargs['coordinates'] kwargs['coordinates'] = inverseCoordinates(coords) kwargs['map'] = self return f(**kwargs) return wrap self.handlers[name] = proxy_handler(handler) else: self.handlers[name] = handler # Update tab children self._update_tab_children() else: print('Tab {} already exists, please choose another name'.format(name)) def removeTab(self, name): """ Remove a tab by its name """ children = self.tab_children_dict.keys() if name in children: self.tab_children_dict.pop(name) self._update_tab_children() def handle_change_tab(self, change): """ Handle function to trigger when tab changes """ # Remove all handlers if change['name'] == 'selected_index': old = change['old'] new = change['new'] old_name = self.tab_widget.get_title(old) new_name = self.tab_widget.get_title(new) # Remove all handlers for handl in self.handlers.values(): self.on_interaction(handl, True) # Set new handler if not None if new_name in self.handlers.keys(): handler = self.handlers[new_name] if handler: self.on_interaction(handler) # Set cursor type if new_name == 'Inspector': self.default_style = {'cursor': 'crosshair'} else: self.default_style = {'cursor': 'grab'} def handle_inspector(self, **change): """ Handle function for the Inspector Widget """ # Get click coordinates coords = change['coordinates'] point_name = 'point inspect at {}'.format(coords) # Widget for adding/removing the point at click def point_widget(coords): coords = inverseCoordinates(coords) add_button = Button(description='ADD', tooltip='add point to map') rem_button = Button(description='REMOVE', tooltip='remove point from map') def add_func(button=None): p = ipyleaflet.Marker(name=point_name, location=coords, draggable=False) self._add_EELayer(point_name, { 'type': 'temp', 'object': None, 'visParams': None, 'layer': p }) def remove_func(button=None): self._remove_EELayer(point_name) add_button.on_click(add_func) rem_button.on_click(remove_func) return HBox([add_button, rem_button]) event = change['type'] # event type if event == 'click': # If the user clicked # create a point where the user clicked point = ee.Geometry.Point(coords) # Get widget thewidget = change['widget'].main # Accordion # First Accordion row text (name) first = 'Point {} at {} zoom'.format(coords, self.zoom) namelist = [first] # wids4acc = [HTML('')] # first row has no content wids4acc = [point_widget(coords)] # Get only Selected Layers in the Inspector Selector selected_layers = dict(zip(self.inspector_wid.selector.label, self.inspector_wid.selector.value)) length = len(selected_layers.keys()) i = 1 for name, obj in selected_layers.items(): # for every added layer # Clear children // Loading thewidget.children = [HTML('wait a second please..')] thewidget.set_title(0, 'Loading {} of {}...'.format(i, length)) i += 1 # Image if obj['type'] == 'Image': # Get the image's values try: image = obj['object'] values = tools.image.getValue(image, point, scale=ZOOM_SCALES[self.zoom], side='client') values = tools.dictionary.sort(values) # Create the content img_html = '' for band, value in values.items(): img_html += '<b>{}</b>: {}</br>'.format(band, value) wid = HTML(img_html) # append widget to list of widgets wids4acc.append(wid) namelist.append(name) except Exception as e: # wid = HTML(str(e).replace('<','{').replace('>','}')) exc_type, exc_value, exc_traceback = sys.exc_info() trace = traceback.format_exception(exc_type, exc_value, exc_traceback) wid = ErrorAccordion(e, trace) wids4acc.append(wid) namelist.append('ERROR at layer {}'.format(name)) # ImageCollection if obj['type'] == 'ImageCollection': # Get the values from all images try: collection = obj['object'] values = tools.imagecollection.getValues( collection, point, scale=ZOOM_SCALES[self.zoom], properties=['system:time_start'], side='client') # header allbands = [val.keys() for bands, val in values.items()] bands = [] for bandlist in allbands: for band in bandlist: if band not in bands: bands.append(band) header = ['image']+bands # rows rows = [] for imgid, val in values.items(): row = ['']*len(header) row[0] = str(imgid) for bandname, bandvalue in val.items(): pos = header.index(bandname) if bandname in header else None if pos: row[pos] = str(bandvalue) rows.append(row) # Create the content html = createHTMLTable(header, rows) wid = HTML(html) # append widget to list of widgets wids4acc.append(wid) namelist.append(name) except Exception as e: exc_type, exc_value, exc_traceback = sys.exc_info() trace = traceback.format_exception(exc_type, exc_value, exc_traceback) wid = ErrorAccordion(e, trace) wids4acc.append(wid) namelist.append('ERROR at layer {}'.format(name)) # Features if obj['type'] == 'Feature': try: feat = obj['object'] feat_geom = feat.geometry() if feat_geom.contains(point).getInfo(): info = featurePropertiesOutput(feat) wid = HTML(info) # append widget to list of widgets wids4acc.append(wid) namelist.append(name) except Exception as e: # wid = HTML(str(e).replace('<','{').replace('>','}')) exc_type, exc_value, exc_traceback = sys.exc_info() trace = traceback.format_exception(exc_type, exc_value, exc_traceback) wid = ErrorAccordion(e, trace) wids4acc.append(wid) namelist.append('ERROR at layer {}'.format(name)) # FeatureCollections if obj['type'] == 'FeatureCollection': try: fc = obj['object'] filtered = fc.filterBounds(point) if filtered.size().getInfo() > 0: feat = ee.Feature(filtered.first()) info = featurePropertiesOutput(feat) wid = HTML(info) # append widget to list of widgets wids4acc.append(wid) namelist.append(name) except Exception as e: wid = HTML(str(e).replace('<','{').replace('>','}')) wids4acc.append(wid) namelist.append('ERROR at layer {}'.format(name)) # Set children and children's name of inspector widget thewidget.children = wids4acc for i, n in enumerate(namelist): thewidget.set_title(i, n) def handle_object_inspector(self, **change): """ Handle function for the Object Inspector Widget DEPRECATED """ event = change['type'] # event type thewidget = change['widget'] if event == 'click': # If the user clicked # Clear children // Loading thewidget.children = [HTML('wait a second please..')] thewidget.set_title(0, 'Loading...') widgets = [] i = 0 for name, obj in self.EELayers.items(): # for every added layer the_object = obj['object'] try: properties = the_object.getInfo() wid = create_accordion(properties) # Accordion wid.selected_index = None # this will unselect all except Exception as e: wid = HTML(str(e)) widgets.append(wid) thewidget.set_title(i, name) i += 1 thewidget.children = widgets def handle_draw(self, dc_widget, action, geo_json): """ Handles drawings """ ty = geo_json['geometry']['type'] coords = geo_json['geometry']['coordinates'] geom = self.draw_types[ty](coords) if action == 'created': self.addGeometry(geom) dc_widget.clear() elif action == 'deleted': for key, val in self.EELayers.items(): if geom == val: self.removeLayer(key) class CustomInspector(HBox): def __init__(self, **kwargs): desc = 'Select one or more layers' super(CustomInspector, self).__init__(description=desc, **kwargs) self.selector = SelectMultiple() self.main = Accordion() self.children = [self.selector, self.main]