diff --git a/MANIFEST b/MANIFEST index ef4dc710..927708e2 100644 --- a/MANIFEST +++ b/MANIFEST @@ -1,7 +1,10 @@ # file GENERATED by distutils, do NOT edit setup.py +sepal_ui/aoi.py sepal_ui/gdal.py +sepal_ui/mapping.py sepal_ui/oft.py +sepal_ui/sepalwidgets.py sepal_ui/widgetBinding.py sepal_ui/widgetFactory.py sepal_ui/scripts/FIPS_code_to_country.csv diff --git a/sepal_ui/aoi.py b/sepal_ui/aoi.py new file mode 100644 index 00000000..d755a225 --- /dev/null +++ b/sepal_ui/aoi.py @@ -0,0 +1,463 @@ +#!/usr/bin/env python3 +from functools import partial +from datetime import datetime +from pathlib import Path +import os + +import ee +import ipyvuetify as v +import geemap +import shapely.geometry as sg +from osgeo import osr, ogr + +from .mapping import SepalMap +from . import sepalwidgets as sw +from . import widgetBinding as wb +from .scripts import run_aoi_selection +from .scripts import messages as ms +from .scripts import utils as su + +ee.Initialize() + + + +class Aoi_io: + + def __init__(self, alert_widget=None): + """Initiate the Aoi object. + + Args: + alert_widget (SepalAlert): Widget to display alerts. + + """ + + # GEE parameters + self.assetId = 'users/dafguerrerom/ReducedAreas_107PHU' + self.column = None + self.field = None + self.selected_feature = None + + #set up your inputs + self.file_input = None + self.file_name = 'Manual_{0}'.format(datetime.now().strftime("%Y-%m-%d_%H-%M-%S")) + self.country_selection = None + self.selection_method = None + self.drawn_feat = None + self.alert = alert_widget + + def get_aoi_ee(self): + + """ Returns an ee.asset from self + + return: ee.Object + + """ + return ee.FeatureCollection(self.assetId) + + def get_columns(self): + + """ Retrieve the columns or variables from self + + return: sorted list cof column names + """ + + aoi_ee = self.get_aoi_ee() + columns = ee.Feature(aoi_ee.first()).propertyNames().getInfo() + columns = sorted([col for col in columns if col not in ['system:index', 'Shape_Area']]) + + return columns + + def get_fields(self, column=None): + """" Retrieve the fields from the selected self column + + Args: + column (str) (optional): Used to query over the asset + + return: sorted list of fields + + """ + + if not column: + column = self.column + + aoi_ee = self.get_aoi_ee() + fields = sorted(aoi_ee.distinct(column).aggregate_array(column).getInfo()) + + return fields + + def get_selected_feature(self): + """ Select a ee object based on the current state. + + Returns: + ee.geometry + + """ + + if not self.column or not self.field: + self.alert.add_msg('error', f'You must first select a column and a field.') + raise + + ee_asset = self.get_aoi_ee() + select_feature = ee_asset.filterMetadata(self.column, 'equals', self.field).geometry() + + # Specify the selected feature + self.selected_feature = select_feature + + return select_feature + + def clear_selected(self): + self.selected_feature = None + + def clear_attributes(self): + + # GEE parameters + self.column = None + self.field = None + self.selected_feature = None + + #set up your inputs + self.file_input = None + self.file_name = 'Manual_{0}'.format(datetime.now().strftime("%Y-%m-%d_%H-%M-%S")) + self.country_selection = None + self.selection_method = None + self.drawn_feat = None + + def get_not_null_attrs(self): + return dict((k, v) for k, v in self.__dict__.items() if v is not None) + + def display_on_map(self, map_): + """ Display the current aoi on a map and remove the dc + + Args: + + map_ (SepalMap): Map to display the element + + """ + aoi = ee.FeatureCollection(self.assetId) + map_.zoom_ee_object(aoi.geometry()) + map_.addLayer(aoi, {'color': 'green'}, name='aoi') + + #the aoi map have a dc + map_.dc.clear() + + try: + map_.remove_control(map_.dc) + except: + pass + + return self + + + def get_bounds(self, ee_asset, cardinal=False): + """ Returns the min(lon,lat) and max(lon, lat) from the given asset + + Args: + ee_asset (ee.object): GEE asset (FeatureCollection, Geometry) + cardinal (boolean) (optional) + + + Returns: + If cardinal True: returns cardinal points tl, bl, tr, br + If cardinal False: returns bounding box + """ + + # + ee_bounds = ee.FeatureCollection(ee_asset).geometry().bounds().coordinates() + coords = ee_bounds.get(0).getInfo() + ll, ur = coords[0], coords[2] + + # Get the bounding box + min_lon, min_lat, max_lon, max_lat = ll[0], ll[1], ur[0], ur[1] + + + # Get (x, y) of the 4 cardinal points + tl = (min_lon, max_lat) + bl = (min_lon, min_lat) + tr = (max_lon, max_lat) + br = (max_lon, min_lat) + + if cardinal: + return tl, bl, tr, br + + return min_lon, min_lat, max_lon, max_lat + + def get_aoi_shp(self, dwnDir=''): + + aoi_name = Path(self.assetId).stem.replace('aoi_', '') + filename = '{0}{1}.shp'.format(dwnDir, aoi_name) + + if os.path.isfile(filename): + return filename + + # verify that the asset exist + aoi = ee.FeatureCollection(self.assetId) + + # convert into shapely + aoiJson = geemap.ee_to_geojson(aoi) + aoiShp = sg.shape(aoiJson['features'][0]['geometry']) + + # Now convert it to a shapefile with OGR + driver = ogr.GetDriverByName('Esri Shapefile') + ds = driver.CreateDataSource(filename) + layer = ds.CreateLayer('', None, ogr.wkbPolygon) + + # Add one attribute + layer.CreateField(ogr.FieldDefn('id', ogr.OFTInteger)) + defn = layer.GetLayerDefn() + + # Create a new feature (attribute and geometry) + feat = ogr.Feature(defn) + feat.SetField('id', 123) + + # Make a geometry, from Shapely object + geom = ogr.CreateGeometryFromWkb(aoiShp.wkb) + feat.SetGeometry(geom) + + layer.CreateFeature(feat) + + # Save and close everything + ds = layer = feat = geom = None + + #add the spatial referecence + spatialRef = osr.SpatialReference() + spatialRef.ImportFromEPSG(4326) + + spatialRef.MorphToESRI() + file = open('{0}{1}.prj'.format(dwnDir, aoi_name), 'w') + file.write(spatialRef.ExportToWkt()) + file.close() + + return filename + +class TileAoi(sw.Tile): + """render and bind all the variable to create an autonomous aoi selector. It will create a asset in you gee account with the name 'aoi_[aoi_name]'. The assetId will be added to io.assetId.""" + + #constants + SELECTION_METHOD =('Country boundaries', 'Draw a shape', 'Upload file', 'Use GEE asset') + + def __init__(self, io, **kwargs): + + #create the output + aoi_output = sw.Alert().add_msg(ms.AOI_MESSAGE) + + #create the inputs widgets + aoi_file_input = v.Select( + items=su.get_shp_files(), + label='Select a file', + v_model=None, + class_='d-none' + ) + wb.bind(aoi_file_input, io, 'file_input', aoi_output) + + aoi_file_name = v.TextField( + label='Select a filename', + v_model=io.file_name, + class_='d-none' + ) + wb.bind(aoi_file_name, io, 'file_name', aoi_output) + + aoi_country_selection = v.Select( + items=[*su.create_FIPS_dic()], + label='Country/Province', + v_model=None, + class_='d-none' + ) + wb.bind(aoi_country_selection, io, 'country_selection', aoi_output) + + aoi_asset_name = v.TextField( + label='Select a GEE asset', + v_model=None, + class_='d-none' + ) + wb.bind(aoi_asset_name, io, 'assetId', aoi_output) + + widget_list = [ + aoi_file_input, + aoi_file_name, + aoi_country_selection, + aoi_asset_name + ] + + #create the map + m = SepalMap(['Esri Satellite', 'CartoDB.Positron'], dc=True) + self.handle_draw(m.dc, io, 'drawn_feat', aoi_output) + + #bind the input to the selected method + aoi_select_method = v.Select(items=self.SELECTION_METHOD, label='AOI selection method', v_model=None) + self.bind_aoi_method(aoi_select_method, widget_list, io, m, m.dc, self.SELECTION_METHOD) + + + #create the validation button + aoi_select_btn = sw.Btn('Select these inputs') + self.bind_aoi_process(aoi_select_btn, io, m, m.dc, aoi_output, self.SELECTION_METHOD) + + #assemble everything on a tile + inputs = v.Layout( + _metadata={'mount-id': 'data-input'}, + class_="pa-5", + row=True, + align_center=True, + children=[ + v.Flex(xs12=True, children=[aoi_select_method]), + v.Flex(xs12=True, children=[aoi_country_selection]), + v.Flex(xs12=True, children=[aoi_file_input]), + v.Flex(xs12=True, children=[aoi_file_name]), + v.Flex(xs12=True, children=[aoi_asset_name]), + v.Flex(xs12=True, children=[aoi_select_btn]), + v.Flex(xs12=True, children=[aoi_output]), + ] + ) + + aoi_content_main = v.Layout( + row=True, + xs12=True, + children = [ + v.Flex(xs12=True, md6=True, children=[inputs]), + v.Flex(class_="pa-5", xs12=True, md6=True, children=[m]) + ] + ) + + super().__init__( + id_='aoi_widget', + title='AOI selection', + inputs=[aoi_content_main] + ) + + def handle_draw(self, dc, io, variable, output): + """ + handle the drawing of a geometry on a map. The geometry is transform into a ee.featurecollection and send to the variable attribute of obj. + + Args: + dc (DrawControl) : the draw control on which the drawing will be done + io (obj Aoi_io) : any object created for IO of your tile + variable (str) : the name of the atrribute of the obj object where to store the ee.FeatureCollection + output (sw.Alert) : the output to display results + + """ + def on_draw(self, action, geo_json, obj, variable, output): + geom = geemap.geojson_to_ee(geo_json, False) + feature = ee.Feature(geom) + setattr(obj, variable, ee.FeatureCollection(feature)) + + output.add_live_msg('A shape have been drawn') + + return + + + dc.on_draw(partial( + on_draw, + obj=io, + variable=variable, + output=output + )) + + return self + + def bind_aoi_process(self, btn, io, m, dc, output, list_method): + """ + Create an asset in your gee acount and serve it to the map. + + Args: + btn (v.Btn) : the btn that launch the process + io (Aoi_IO) : the IO of the aoi selection tile + m (geemap.Map) : the tile map + dc (drawcontrol) : the drawcontrol + output (v.Alert) : the alert of the selector tile + list_method([str]) : the list of the available selections methods + """ + + def on_click(widget, event, data, io, m, dc, output, list_method): + + widget.toggle_loading() + + #create the aoi asset + assetId = run_aoi_selection.run_aoi_selection( + file_input = io.file_input, + file_name = io.file_name, + country_selection = io.country_selection, + asset_name = io.assetId, + drawn_feat = io.drawn_feat, + drawing_method = io.selection_method, + widget_alert = output, + list_method = list_method, + ) + + #remove the dc + dc.clear() + try: + m.remove_control(dc) + except: + pass + + #display it on the map + if assetId: + setattr(io, 'assetId', assetId) + io.display_on_map(m) + + widget.toggle_loading() + + return + + btn.on_event('click', partial( + on_click, + io = io, + m = m, + dc = dc, + output = output, + list_method = list_method + )) + + return self + + def bind_aoi_method(self, method_widget, list_input, obj, m, dc, selection_method): + """ + change the display of the AOI selector according to the method selected. will only display the useful one + + Args: + method_widget (v.select) : the method selector widget + list_input ([v.widget]) : the list of all the aoi inputs + obj (Aoi_IO) : the IO object of the tile + m (geemap.Map) the map displayed in the tile + dc (DrawControl) : the drawing control + selection_method ([str]) : the available selection methods + """ + + def on_change(widget, event, data, list_input, obj, m, dc, selection_method): + + #clearly identify the differents widgets + aoi_file_input = list_input[0] + aoi_file_name = list_input[1] + aoi_country_selection = list_input[2] + aoi_asset_name = list_input[3] + + setattr(obj, 'selection_method', widget.v_model) + + #remove the dc + dc.clear() + try: + m.remove_control(dc) + except: + pass + #toogle the appropriate inputs + if widget.v_model == selection_method[0]: #country selection + self.toggle_inputs([aoi_country_selection], list_input) + elif widget.v_model == selection_method[1]: #drawing + self.toggle_inputs([aoi_file_name], list_input) + m.add_control(dc) + elif widget.v_model == selection_method[2]: #shp file + self.toggle_inputs([aoi_file_input], list_input) + elif widget.v_model == selection_method[3]: #gee asset + self.toggle_inputs([aoi_asset_name], list_input) + + + + method_widget.on_event('change', partial( + on_change, + list_input=list_input, + obj=obj, + m=m, + dc=dc, + selection_method=selection_method + )) + + return self \ No newline at end of file diff --git a/sepal_ui/gdal.py b/sepal_ui/gdal.py index f7b98f4e..1f43d38e 100644 --- a/sepal_ui/gdal.py +++ b/sepal_ui/gdal.py @@ -1,4 +1,5 @@ from sepal_ui.scripts import utils as su +import string def merge(input_files, out_filename=None, out_format=None, co=None, pixelsize=None, tap=False, separate=False, v=False, pct=False, extents=None, nodata_value=None, output_nodata_value=None, datatype=None, output=None): """ @@ -88,4 +89,54 @@ def merge(input_files, out_filename=None, out_format=None, co=None, pixelsize=No command += input_files - return su.launch(command, output) \ No newline at end of file + return su.launch(command, output) + +def calc(expression, inputs, out_file, bands=None, no_data=None, type_=None, format_=None, co=None, overwrite=False, output=None): + """ + Command line raster calculator with numpy syntax. Use any basic arithmetic supported by numpy arrays such as +, -, *, and \ along with logical operators such as >. Note that all files must have the same dimensions, but no projection checking is performed. + + Args: + expression (str): Calculation in gdalnumeric syntax using +, -, /, *, or any numpy array functions (i.e. log10()). + inputs ([raster]): Input gdal raster file, you can use any letter (A-Z). + bands ([int], optionnal): Number of raster band for files. must be the same size as inputs with None value for defaults ex: [1,2,None,5]. Default value is one. + output (str): Output file to generate or fill. + no_data (int, optionnal): Output nodata value (default datatype specific value). + type_ (str, optionnal): Output datatype, must be one of [Int32, Int16, Float64, UInt16, Byte, UInt32, Float32]. + format_ (str, optionnal): GDAL format for output file + co (str, optionnal): Passes a creation option to the output format driver. Multiple options may be listed. See format specific documentation for legal creation options for each format. + overwrite (bool): Overwrite output file if it already exists. + output (v.alert, optional): the alert where to display the output + """ + + command = ['gdal_calc.py'] + + command += ['--calc="{}"'.format(expression)] + + for i in max(len(inputs, 26)): + command += ['-' + string.ascii_uppercase[i], inputs[i]] + + command += ['--outputfile={}'.format(out_file)] + + if bands and len(bands) == len(inputs): + for i in max(len(bands), 26): + command +=['-{0}_band={1}'.format(string.ascii_uppercase[i], bands[i])] + + if no_data: + command += ['--NoDataValue={}'.format(no_data)] + + types = ['Int32', 'Int16', 'Float64', 'UInt16', 'Byte', 'UInt32', 'Float32'] + if type_ and (type_ in types): + command += ['--type={}'.format(type_)] + + if format_: + command += ['--format={}'.format(format_)] + + if co: + command += ['--co={}'.format(co)] + + if overwrite == True: + command += ['--overwrite'] + + return su.launch(command, output) + + \ No newline at end of file diff --git a/sepal_ui/mapping.py b/sepal_ui/mapping.py new file mode 100644 index 00000000..8da833b8 --- /dev/null +++ b/sepal_ui/mapping.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +import os +del os.environ['GDAL_DATA'] + +import geemap +import ee +from haversine import haversine +import xarray_leaflet +import numpy as np +import rioxarray +import xarray as xr +import matplotlib.pyplot as plt +from sepal_ui.scripts import utils as su + +#initialize earth engine +ee.Initialize() + + +class SepalMap(geemap.Map): + """initialize differents maps and tools""" + + def __init__(self, basemaps=[], dc=False, **kwargs): + + super().__init__( + add_google_map=False, + center = [0,0], + zoom = 2, + **kwargs) + + #add the basemaps + self.clear_layers() + if len(basemaps): + for basemap in basemaps: + self.add_basemap(basemap) + else: + self.add_basemap('CartoDB.DarkMatter') + + #add the base controls + self.clear_controls() + self.add_control(geemap.ZoomControl(position='topright')) + self.add_control(geemap.LayersControl(position='topright')) + self.add_control(geemap.AttributionControl(position='bottomleft')) + self.add_control(geemap.ScaleControl(position='bottomleft', imperial=False)) + + #specific drawing control + self.set_drawing_controls(dc) + + def set_drawing_controls(self, bool_=True): + if bool_: + dc = geemap.DrawControl( + marker={}, + circlemarker={}, + polyline={}, + rectangle={'shapeOptions': {'color': '#0000FF'}}, + circle={'shapeOptions': {'color': '#0000FF'}}, + polygon={'shapeOptions': {'color': '#0000FF'}}, + ) + else: + dc = None + + self.dc = dc + + return self + + def remove_last_layer(self): + + if len(self.layers) > 1: + last_layer = self.layers[-1] + self.remove_layer(last_layer) + + + def zoom_ee_object(self, ee_geometry, zoom_out=1): + + #center the image + self.centerObject(ee_geometry) + + #extract bounds from ee_object + ee_bounds = ee_geometry.bounds().coordinates() + coords = ee_bounds.get(0).getInfo() + ll, ur = coords[0], coords[2] + + # Get the bounding box + min_lon, min_lat, max_lon, max_lat = ll[0], ll[1], ur[0], ur[1] + + + # Get (x, y) of the 4 cardinal points + tl = (max_lat, min_lon) + bl = (min_lat, min_lon) + tr = (max_lat, max_lon) + br = (min_lat, max_lon) + + #zoom on these bounds + self.zoom_bounds([tl, bl, tr, br], zoom_out) + + return self + + def zoom_bounds(self, bounds, zoom_out=1): + """ + Get the proper zoom to the given bounds. + + Args: + + bounds (list of tuple(x,y)): coordinates of tl, bl, tr, br points + zoom_out (int) (optional): Zoom out the bounding zoom + """ + + tl, bl, tr, br = bounds + + maxsize = max(haversine(tl, br), haversine(bl, tr)) + + lg = 40075 #number of displayed km at zoom 1 + zoom = 1 + while lg > maxsize: + zoom += 1 + lg /= 2 + + if zoom_out > zoom: + zoom_out = zoom - 1 + + self.zoom = zoom-zoom_out + + return self + + def update_map(self, assetId, bounds, remove_last=False): + """Update the map with the asset overlay and removing the selected drawing controls + + Args: + assetId (str): the asset ID in gee assets + bounds (list of tuple(x,y)): coordinates of tl, bl, tr, br points + remove_last (boolean) (optional): Remove the last layer (if there is one) before + updating the map + """ + if remove_last: + self.remove_last_layer() + + self.set_zoom(bounds, zoom_out=2) + self.centerObject(ee.FeatureCollection(assetId), zoom=self.zoom) + self.addLayer(ee.FeatureCollection(assetId), {'color': 'green'}, name='aoi') + + return self + + #copy of the geemap add_raster function to prevent a bug from sepal + def add_raster(self, image, bands=None, layer_name=None, colormap=None, x_dim='x', y_dim='y', opacity=1.0): + """Adds a local raster dataset to the map. + Args: + image (str): The image file path. + bands (int or list, optional): The image bands to use. It can be either a number (e.g., 1) or a list (e.g., [3, 2, 1]). Defaults to None. + layer_name (str, optional): The layer name to use for the raster. Defaults to None. + colormap (str, optional): The name of the colormap to use for the raster, such as 'gray' and 'terrain'. More can be found at https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html. Defaults to None. + x_dim (str, optional): The x dimension. Defaults to 'x'. + y_dim (str, optional): The y dimension. Defaults to 'y'. + """ + if not os.path.exists(image): + print('The image file does not exist.') + return + + if colormap is None: + colormap = plt.cm.inferno + + if layer_name is None: + layer_name = 'Layer_' + su.random_string() + + if isinstance(colormap, str): + colormap = plt.cm.get_cmap(name=colormap) + + da = rioxarray.open_rasterio(image, masked=True) + + multi_band = False + if len(da.band) > 1: + multi_band = True + if bands is None: + bands = [3, 2, 1] + else: + bands = 1 + + if multi_band: + da = da.rio.write_nodata(0) + else: + da = da.rio.write_nodata(np.nan) + da = da.sel(band=bands) + + if multi_band: + layer = da.leaflet.plot( + self, x_dim=x_dim, y_dim=y_dim, rgb_dim='band') + else: + layer = da.leaflet.plot( + self, x_dim=x_dim, y_dim=y_dim, colormap=colormap) + + layer.name = layer_name + + layer.opacity = opacity if abs(opacity) <= 1.0 else 1.0 + + + return self \ No newline at end of file diff --git a/sepal_ui/scripts/utils.py b/sepal_ui/scripts/utils.py index 92af72c1..bc8c0357 100644 --- a/sepal_ui/scripts/utils.py +++ b/sepal_ui/scripts/utils.py @@ -6,6 +6,8 @@ import csv from urllib.parse import urlparse import subprocess +import string +import random import ipyvuetify as v @@ -113,4 +115,15 @@ def launch(command, output=None): if output: displayIO(output, line) - return output_txt \ No newline at end of file + return output_txt + +def random_string(string_length=3): + """Generates a random string of fixed length. + Args: + string_length (int, optional): Fixed length. Defaults to 3. + Returns: + str: A random string + """ + # random.seed(1001) + letters = string.ascii_lowercase + return ''.join(random.choice(letters) for i in range(string_length)) \ No newline at end of file diff --git a/sepal_ui/sepalwidgets.py b/sepal_ui/sepalwidgets.py new file mode 100644 index 00000000..be56f7ec --- /dev/null +++ b/sepal_ui/sepalwidgets.py @@ -0,0 +1,543 @@ +#!/usr/bin/env python3 +from functools import partial +from markdown import markdown +from datetime import datetime +import traitlets +import os + +import ipyvuetify as v + +from sepal_ui.scripts import utils +from sepal_ui.scripts import messages as ms + +############################ +## hard coded colors ## +############################ + +sepal_main = '#2e7d32' +sepal_darker = '#005005' + +########################### +## classes ## +########################### +class SepalWidget(v.VuetifyWidget): + + def __init__(self, **kwargs): + + super().__init__(**kwargs) + self.viz = True + + def toggle_viz(self): + """toogle the visibility of the widget""" + if self.viz: + self.hide() + else: + self.show() + + return self + + def hide(self): + """add the d-none html class to the widget""" + if not 'd-none' in str(self.class_): + self.class_ = str(self.class_).strip() + ' d-none' + self.viz = False + + return self + + def show(self): + """ remove the d-none html class to the widget""" + if 'd-none' in str(self.class_): + self.class_ = str(self.class_).replace('d-none', '') + self.viz = True + + return self + +class Alert(v.Alert, SepalWidget): + """create an alert widget that can be used to display the process outputs""" + + TYPES = ('info', 'secondary', 'primary', 'error', 'warning', 'success') + + def __init__(self, type_=None, **kwargs): + + type_ = type_ if (type_ in self.TYPES) else self.TYPES[0] + + super().__init__( + children = [''], + type = type_, + text = True, + class_="mt-5", + **kwargs + ) + + self.hide() + + + def add_msg(self, msg, type_='info'): + self.show() + self.type = type_ if (type_ in self.TYPES) else self.TYPES[0] + self.children = [msg] + + return self + + def add_live_msg(self, msg, type_='info'): + + current_time = datetime.now().strftime("%Y/%m/%d, %H:%M:%S") + + self.show() + self.type = type_ if (type_ in self.TYPES) else self.TYPES[0] + + self.children = [ + v.Html(tag='p', children=['[{}]'.format(current_time)]), + v.Html(tag='p', children=[msg]) + ] + + return self + + def reset(self): + self.children = [''] + self.hide() + + return self + + def bind(self, widget, obj, variable, msg=None): + """ + bind the variable to the widget and display it in the alert + + Args: + widget (v.XX) : an ipyvuetify input element + obj : the process_io object + variable (str) : the name of the member in process_io object + output_message (str, optionnal) : the output message before the variable display + """ + if not msg: msg = 'The selected variable is: ' + + def on_change(widget, event, data, obj, variable, output, msg): + + setattr(obj, variable, widget.v_model) + + msg += str(widget.v_model) + output.add_msg(msg) + + return + + widget.on_event('change', partial( + on_change, + obj=obj, + variable=variable, + output=self, + msg=msg + )) + + return self + + def check_input(self, input_, msg=None): + """ + Check if the inpupt value is initialized. If not return false and display an error message else return True + + Args: + input_ : the input to check + msg (str, optionnal): the message to display if the input is not set + + Returns: + (bool): check if the value is initialized + """ + if not msg: msg = "The value has not been initialized" + init = True + + if input_ == None: + init = False + self.add_msg(msg, 'error') + + return init + + +class Btn(v.Btn, SepalWidget): + """ + Creates a process button filled with the provided text + + Returns: + btn (v.Btn) : + """ + + + def __init__(self, text='Click', icon=None, **kwargs): + super().__init__(**kwargs) + self.color='primary' + self.v_icon = None + self.children=[text] + + if icon: + self.set_icon(icon) + + def set_icon(self, icon): + + if self.v_icon: + self.v_icon.children = [icon] + else: + self.v_icon = v.Icon(left=True, children=[icon]) + self.children = [self.v_icon] + self.children + + return self + + def toggle_loading(self): + """disable and start loading or reverse""" + self.loading = not self.loading + self.disabled = self.loading + + return self + +class AppBar (v.AppBar, SepalWidget): + """create an appBar widget with the provided title using the sepal color framework""" + def __init__(self, title='SEPAL module', **kwargs): + + self.toggle_button = v.Btn( + icon = True, + children=[ + v.Icon(class_="white--text", children=['mdi-dots-vertical']) + ] + ) + + super().__init__( + color=sepal_main, + class_="white--text", + dense=True, + app = True, + children = [self.toggle_button, v.ToolbarTitle(children=[title])], + **kwargs + ) + + def setTitle(self, title): + """set the title in the appbar""" + + self.children = [ + self.toolBarButton, + v.ToolbarTitle(children=[title]) + ] + + return self + +class DrawerItem(v.ListItem, SepalWidget): + """create a drawer item using the user input""" + + def __init__(self, title, icon=None, card='', href='', **kwargs): + + icon = icon if icon else 'mdi-folder-outline' + + children = [ + v.ListItemAction( + children=[ + v.Icon( + class_="white--text", + children=[icon]) + ] + ), + v.ListItemContent( + children=[ + v.ListItemTitle( + class_="white--text", + children=[title] + ) + ] + ) + ] + + super().__init__( + link=True, + children=children, + **kwargs) + + if not href == '': + self.href=href + self.target="_blank" + + if not card == '': + self._metadata = {'card_id': card } + + def display_tile(self, tiles): + """ + display the apropriate tiles when the item is clicked + + Args: + tiles ([v.Layout]) : the list of all the available tiles in the app + """ + def on_click(widget, event, data, tiles): + for tile in tiles: + if widget._metadata['card_id'] == tile._metadata['mount_id']: + tile.show() + else: + tile.hide() + + self.on_event('click', partial(on_click, tiles=tiles)) + + return self + +class NavDrawer(v.NavigationDrawer, SepalWidget): + """ + create a navdrawer using the different items of the user and the sepal color framework. The drawer can include links to the github page of the project for wiki, bugs and repository. + """ + + def __init__(self, items, code=None, wiki=None, issue=None, **kwargs): + + code_link = [] + if code: + item_code = DrawerItem('Source code', icon='mdi-file-code', href=code) + code_link.append(item_code) + if wiki: + item_wiki = DrawerItem('Wiki', icon='mdi-book-open-page-variant', href=wiki) + code_link.append(item_wiki) + if issue: + item_bug = DrawerItem('Bug report', icon='mdi-bug', href=issue) + code_link.append(item_bug) + + super().__init__( + v_model=True, + app=True, + color = sepal_darker, + children = [ + v.List(dense=True, children=items), + v.Divider(), + v.List(dense=True, children=code_link) + ], + **kwargs + ) + + def display_drawer(self, toggleButton): + """ + bind the drawer to it's toggleButton + + Args: + drawer (v.navigationDrawer) : the drawer tobe displayed + toggleButton(v.Btn) : the button that activate the drawer + """ + def on_click(widget, event, data, drawer): + drawer.v_model = not drawer.v_model + + toggleButton.on_event('click', partial(on_click, drawer=self)) + + return self + +class Footer(v.Footer, SepalWidget): + """create a footer with cuzomizable text. Not yet capable of displaying logos""" + def __init__(self, text="", **kwargs): + + text = text if text != '' else 'SEPAL \u00A9 {}'.format(datetime.today().year) + + super().__init__( + color = sepal_main, + class_ = "white--text", + app=True, + children = [text], + **kwargs + ) + +class App (v.App, SepalWidget): + """Create an app display with the tiles created by the user. Display false footer and appBar if not filled. navdrawer is fully optionnal + """ + + def __init__(self, tiles=[''], appBar=None, footer=None, navDrawer=None, **kwargs): + + self.tiles = None if tiles == [''] else tiles + + app_children = [] + + #add the navDrawer if existing + if navDrawer: + app_children.append(navDrawer) + + #create a false appBar if necessary + if not appBar: + appBar = AppBar() + app_children.append(appBar) + + #add the content of the app + content = v.Content(children=[ + v.Container(fluid=True,children = tiles) + ]) + app_children.append(content) + + #create a false footer if necessary + if not footer: + footer = Footer() + app_children.append(footer) + + super().__init__( + v_model=None, + children = app_children, + **kwargs) + + def show_tile(self, name): + """select the tile to display using its mount-id""" + for tile in self.tiles: + if name == tile._metadata['mount_id']: + tile.show() + else: + tile.hide() + + return self + + +class Tile(v.Layout, SepalWidget): + """create a customizable tile for the sepal UI framework""" + + def __init__(self, id_, title, inputs=[''], btn=None, output=None, **kwargs): + + if btn: + inputs.append(btn) + + if output: + inputs.append(output) + + title = v.Html(xs12=True, tag='h2', children=[title]) + content = [v.Flex(xs12=True, children=[widget]) for widget in inputs] + + card = v.Card( + class_ = "pa-5", + raised = True, + xs12 = True, + children = [title] + content + ) + + super().__init__( + _metadata={'mount_id': id_}, + row=True, + align_center=True, + class_="ma-5 d-inline", + xs12=True, + children = [card], + **kwargs + ) + + def set_content(self, content): + + self.children[0].children = [self.children[0].children[0]] + content + + return self + + def set_title(self, title): + + title = v.Html(xs12=True, tag='h2', children=[title]) + + self.children[0].children = [title] + self.children[0].children[1:] + + return self + + def hide(self): + """hide the widget""" + + super().hide() + + if 'd-inline' in str(self.class_): + self.class_ = self.class_.replace('d-inline','') + + return self + + def show(self): + """ remove the d-none html class to the widget""" + + super().show() + + if not 'd-inline' in str(self.class_): + self.class_ = str(self.class_).strip() + ' d-inline' + + return self + + def toggle_inputs(self, fields_2_show, fields): + """ + display only the widgets that are part of the input_list. the widget_list is the list of all the widgets of the tile. + + Args: + fields_2_show ([v.widget]) : the list of input to be display + fields ([v.widget]) : the list of the tile widget + """ + + for field in fields: + if field in fields_2_show: + if 'd-none' in str(field.class_): + field.class_ = field.class_.replace('d-none', '') + else: + if not 'd-none' in str(field.class_): + field.class_ = str(field.class_).strip() + ' d-none' + + + return self + +class TileAbout(Tile): + """ + create a about tile using a md file. This tile will have the "about_widget" id and "About" title.""" + + def __init__(self, pathname, **kwargs): + + #read the content and transform it into a html + f = open(pathname, 'r') + if f.mode == 'r': + about = f.read() + else : + about = '**No About File**' + + about = markdown(about, extensions=['fenced_code','sane_lists']) + + #need to be nested in a div to be displayed + about = '
\n' + about + '\n
' + + #create a Html widget + class MyHTML(v.VuetifyTemplate): + template = traitlets.Unicode(about).tag(sync=True) + + + content = MyHTML() + + super().__init__('about_widget', 'About', inputs=[content], **kwargs) + +class TileDisclaimer(Tile): + """ + create a about tile using a md file. This tile will have the "about_widget" id and "About" title.""" + + def __init__(self, **kwargs): + + pathname = os.path.join(os.path.dirname(__file__), 'scripts', 'disclaimer.md') + + #read the content and transform it into a html + f = open(pathname, 'r') + if f.mode == 'r': + about = f.read() + else : + about = '**No Disclaimer File**' + + about = markdown(about, extensions=['fenced_code','sane_lists']) + + #need to be nested in a div to be displayed + about = '
\n' + about + '\n
' + + #create a Html widget + class MyHTML(v.VuetifyTemplate): + template = traitlets.Unicode(about).tag(sync=True) + + + content = MyHTML() + + super().__init__('about_widget', 'Disclaimer', inputs=[content], **kwargs) + + +class DownloadBtn(v.Btn, SepalWidget): + """Create a green downloading button with the user text""" + + def __init__(self, text, path='#', **kwargs): + + #create the url + if utils.is_absolute(path): + url = path + else: + url = utils.create_download_link(path) + + super().__init__( + class_='ma-2', + xs5=True, + color='success', + href=url, + children=[ + v.Icon(left=True, children=['mdi-download']), + text + ], + **kwargs + ) + \ No newline at end of file diff --git a/setup.py b/setup.py index 45e62aee..6efaa6e9 100644 --- a/setup.py +++ b/setup.py @@ -4,13 +4,13 @@ name = 'sepal_ui', packages = ['sepal_ui', 'sepal_ui.scripts'], package_data={'sepal_ui': ['scripts/*.csv', 'scripts/*.md']}, - version = '0.4-beta', + version = '0.6-beta', license='MIT', description = 'wrapper for ipyvuetify widgets to unify the display of voila dashboards in the sepal plateform', author = 'Pierrick Rambaud', author_email = 'pierrick.rambaud49@gmail.com', url = 'https://github.com/12rambau/sepal_ui', - download_url = 'https://github.com/12rambau/sepal_ui/archive/v_0.5-beta.tar.gz', + download_url = 'https://github.com/12rambau/sepal_ui/archive/v_0.6-beta.tar.gz', keywords = ['UI', 'Python', 'widget', 'sepal'], install_requires=[ 'haversine',