<jupyter_start><jupyter_text>Hydrological Landscape Classes (HLC) generator app Application to download HLC map in any region via Google Earth Engine (GEE) Developed by <NAME> (2021)For more information, access the GitHub page: https://github.com/rbfont/hlc-generatorThis application has some limitations. If your region of study is too large or if you want more flexibility with the definitions, please use the JavaScript code directly in GEE platform: https://code.earthengine.google.com/f3c135517e2387fcd4f9abc5baf54f72For questions and suggestions, please contact: <EMAIL><jupyter_code>from ipywidgets import widgets, Dropdown, Layout
from ipyleaflet import WidgetControl

def linked_maps_wlegend(
    rows=2,
    cols=2,
    height="400px",
    ee_objects=[],
    vis_params=[],
    labels=[],
    legends=[],
    label_position="topright",
    **kwargs,
                ):
    """Create linked maps of Earth Engine data layers.

    Args:
        rows (int, optional): The number of rows of maps to create. Defaults to 2.
        cols (int, optional): The number of columns of maps to ceate. Defaults to 2.
        height (str, optional): The height of each map in pixels. Defaults to "400px".
        ee_objects (list, optional): The list of Earth Engine objects to use for each map. Defaults to [].
        vis_params (list, optional): The list of visualization parameters to use for each map. Defaults to [].
        labels (list, optional): The list of labels to show on the map. Defaults to [].
        label_position (str, optional): The position of the label, can be [topleft, topright, bottomleft, bottomright]. Defaults to "topright".

    Raises:
        ValueError: If the length of ee_objects is not equal to rows*cols.
        ValueError: If the length of vis_params is not equal to rows*cols.
        ValueError: If the length of labels is not equal to rows*cols.

    Returns:
        ipywidget: A GridspecLayout widget.
    """
    grid = widgets.GridspecLayout(rows, cols, grid_gap="0px")
    count = rows * cols

    maps = []

    if len(ee_objects) > 0:
        if len(ee_objects) == 1:
            ee_objects = ee_objects * count
        elif len(ee_objects) < count:
            raise ValueError(f"The length of ee_objects must be equal to {count}.")

    if len(vis_params) > 0:
        if len(vis_params) == 1:
            vis_params = vis_params * count
        elif len(vis_params) < count:
            raise ValueError(f"The length of vis_params must be equal to {count}.")

    if len(labels) > 0:
        if len(labels) == 1:
            labels = labels * count
        elif len(labels) < count:
            raise ValueError(f"The length of labels must be equal to {count}.")

    for i in range(rows):
        for j in range(cols):
            index = i * rows + j
            m = geemap.Map(
                height=height,
                lite_mode=True,
                add_google_map=False,
                layout=widgets.Layout(margin="0px", padding="0px"),
                **kwargs,
            )

            if len(ee_objects) > 0:
                m.addLayer(ee_objects[index], vis_params[index], labels[index])
                m.add_legend(legend_title=" ", legend_dict=legends[index])

            if len(labels) > 0:
                label = widgets.Label(
                    labels[index], layout=widgets.Layout(padding="0px 5px 0px 5px")
                )
                control = WidgetControl(widget=label, position=label_position)
                m.add_control(control)

            maps.append(m)
            widgets.jslink((maps[0], "center"), (m, "center"))
            widgets.jslink((maps[0], "zoom"), (m, "zoom"))

            output = widgets.Output()
            with output:
                display(m)
            grid[i, j] = output

    return grid

def ee_export_image_zip(
    ee_object, filename, scale=None, crs=None, region=None, file_per_band=False
):
    """Exports an ee.Image as a GeoTIFF.

    Args:
        ee_object (object): The ee.Image to download.
        filename (str): Output filename for the exported image.
        scale (float, optional): A default scale to use for any bands that do not specify one; ignored if crs and crs_transform is specified. Defaults to None.
        crs (str, optional): A default CRS string to use for any bands that do not explicitly specify one. Defaults to None.
        region (object, optional): A polygon specifying a region to download; ignored if crs and crs_transform is specified. Defaults to None.
        file_per_band (bool, optional): Whether to produce a different GeoTIFF per band. Defaults to False.
    """
    import os
    import requests

    if not isinstance(ee_object, ee.Image):
        print("The ee_object must be an ee.Image.")
        return

    filename = os.path.abspath(filename)
    basename = os.path.basename(filename)
    name = os.path.splitext(basename)[0]
    filetype = os.path.splitext(basename)[1][1:].lower()
    filename_zip = filename.replace(".tif", ".zip")

    if filetype != "tif":
        print("The filename must end with .tif")
        return

    try:
        print("Generating URL ...")
        params = {"name": name, "filePerBand": file_per_band}
        if scale is None:
            scale = ee_object.projection().nominalScale().multiply(10)
        params["scale"] = scale
        if region is None:
            region = ee_object.geometry()
        params["region"] = region
        if crs is not None:
            params["crs"] = crs

        url = ee_object.getDownloadURL(params)
        print("Click link to download data: {}".format(url))
        r = requests.get(url, stream=True)

        if r.status_code != 200:
            print("An error occurred while downloading.")
            return

        with open(filename_zip, "wb") as fd:
            for chunk in r.iter_content(chunk_size=1024):
                fd.write(chunk)

    except Exception as e:
        print("An error occurred while requesting.")
        print(e)
        return

import numpy as np
'''
The app is organized in a nested functions scheme
Perhaps not the most elegant way, but it is a lot easier to understand the process (and to perform changes)
Funtion 1 (inputs) receives input maps and the scale parameter
    and returns the first year available of Land Cover database
    and the region of interest (ROI) extension selection
Function 2 (roi_from_coords) receives extension of ROI coordinates
    and returns widgets for parameters selection
Function 3 (params) receives the parameter definition and
    performs computations on map visualization and downloading
'''

def inputs(DEM, HAND, LandCover, Scale):
    def roi_from_coords(west, south, east, north):
        def params(Slope_th,
                   HAND_th,
                   LC_year):
            
            # Image manipulation
            slpTh = Slope_th
            handTh = HAND_th
            lcY = LC_year

            tc_img = ee.Image(1).where(
                        slp_img.gt(slpTh).And(hand_img.gt(handTh)), 2).where(
                        slp_img.lte(slpTh).And(hand_img.gt(handTh)), 3)#.reproject(crs='EPSG:4326', scale=scale)
            
            if lc_imgcol == mapbiomas:
                old_classes = [1, 2, 3, 4, 5, 9, 10, 11, 12, 13, 14, 15, 18, 19, 39, 20, 41, 36, 21, 22, 23, 24, 25, 26, 29, 30, 31, 32, 33]
                new_classes = [1, 1, 1, 2, 1, 1,  2,  2,  2,  2,  3,  2,  3,  3,  3,  3,  3,  3,  3,  4,  4,  4,  4,  5,  4,  4,  5,  4,  5]
            elif lc_imgcol == igbp:
                old_classes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
                new_classes = [1, 1, 1, 1, 1, 2, 2, 2, 2,  2,  2,  3,  4,  2,  4,  4,  5]
            elif lc_imgcol == umd:
                old_classes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
                new_classes = [5, 1, 1, 1, 1, 1, 2, 2, 2, 2,  2,  2,  3,  4,  2,  4]
            elif lc_imgcol == lai:
                old_classes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
                new_classes = [5, 2, 2, 3, 2, 1, 1, 1, 1, 4,  4]
            elif lc_imgcol == pft:
                old_classes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
                new_classes = [5, 1, 1, 1, 1, 2, 2, 3, 3, 4,  4,  4]
            
            try:
                lcr_img = lc_imgcol.filter(ee.Filter.calendarRange(lcY, lcY, 'year')).first()
            except:
                lcr_img = lc_imgcol.select('classification_'+ str(lcY))
            
            lc_img = lcr_img.remap(old_classes, new_classes)
            lc_img = ee.Image(5).where(lc_img.lte(5), lc_img)
            lc_img = ee.Image(1).where(lc_img.gt(1), lc_img)
            
            # HAND + Slope + Land cover
            #
            #  '1': Wetland Forest
            #  '2': Wetland non-Forest natural
            #  '3': Wetland Farmland
            #  '4': Hillslope Forest
            #  '5': Hillslope non-Forest natural
            #  '6': Hillslope Farmland
            #  '7': Plateau Forest
            #  '8': Plateau non-Forest natural
            #  '9': Plateau Farmland
            #  '10': Semi-permeable
            #  '11': Water
            #
            hlc_img = tc_img.multiply(10).addBands(lc_img).reduce(ee.Reducer.sum())
            hlc_img = hlc_img.where(
                hlc_img.remap([14, 24, 34], [1, 1, 1]), 40).where(
                hlc_img.remap([15, 25, 35], [1, 1, 1]), 50)
            hlc_img = hlc_img.remap([11, 12, 13, 21, 22, 23, 31, 32, 33, 40, 50],
                                    [ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
            hlc_img = ee.Image(1).where(hlc_img.gt(1), hlc_img)#.reproject(crs='EPSG:4326', scale=scale)
            
            # Render maps -------------------------------------------------------
            tc_legend = {
                        'Wetland': "#aec3d4", 
                        'Hillslope': "#cc0013", 
                        'Plateau': "#369b47"
                        }
            parvis_tc = {'palette': list(tc_legend.values())}
            #Map.addLayer(tc_img, parvis, 'Terrain Classes')

            lc_legend = {
                        'Forest': "#152106",
                        'Savanna-Grassland': "#387242",
                        'Farmland': "#cdb33b",
                        'Non-vegetated': "#cc0013",
                        'Water': "#aec3d4"
                        }        
            parvis_lc = {'palette': list(lc_legend.values())}
            #Map.addLayer(lc_img, parvis, 'Land Cover Classes')


            hlc_legend = {
                        "01 Wetland Forest": "#152106",
                        "02 Wetland Savanna-Grassland": "#387242",
                        "03 Wetland Farmland": "#d9903d",
                        "04 Hillslope Forest": "#225129",
                        "05 Hillslope Savanna-Grassland": "#c3aa69",
                        "06 Hillslope Farmland": "#cdb33b",
                        "07 Plateau Forest": "#6a2325",
                        "08 Plateau Savanna-Grassland": "#369b47",
                        "09 Plateau Farmland": "#91af40",
                        "10 Non-vegetated": "#cc0013",
                        "11 Water": "#aec3d4"
                        }
            parvis_hlc = {'palette': list(hlc_legend.values())}

            imgs = [tc_img, lc_img, hlc_img]
            parvis_w = [parvis_tc, parvis_lc, parvis_hlc]
            labels = ['Terrain Classes', 'Land Cover Classes', 'Hydrological Landscape Classes']
            legends = [tc_legend, lc_legend, hlc_legend]

            Maps = linked_maps_wlegend(rows=1, cols=3, height="600px", center=centroid, zoom=7,
                                ee_objects=imgs, vis_params=parvis_w, labels=labels, legends=legends, label_position="topright")
            
            display(Maps)
            
            # Buttons to download data --------------------------------------------------------
            btn_layout = Layout(width='500px')
            dem_button = widgets.Button(description='Download DEM map', layout=btn_layout)
            slp_button = widgets.Button(description='Download Slope map', layout=btn_layout)
            hand_button = widgets.Button(description='Download HAND map', layout=btn_layout)
            lcr_button = widgets.Button(description='Download Land Cover map', layout=btn_layout)
            buttons_raw = widgets.VBox([dem_button, slp_button, hand_button, lcr_button], layout=Layout(grid_gap='3px'))
            
            tc_button = widgets.Button(description='Download Terrain Classes map', layout=btn_layout)
            lc_button = widgets.Button(description='Download Land Cover Classes map (reclassified)', layout=btn_layout)
            hlc_button = widgets.Button(description='Download Hydrological Landscape Classes map', layout=btn_layout)
            buttons = widgets.VBox([tc_button, lc_button, hlc_button], layout=Layout(grid_gap='3px'))
            
            print('\n\n\nDownload raw maps:')
            display(buttons_raw)            
            
            print('\n\n\nDownload processed maps:')
            display(buttons)

            out = widgets.Output()

            def dwn_dem(_):
                with out:
                    ee_export_image_zip(dem_img, 'dem_map.tif', scale=scale, region=roi)            
            def dwn_slp(_):
                with out:
                    ee_export_image_zip(slp_img, 'slp_map.tif', scale=scale, region=roi)            
            def dwn_hand(_):
                with out:
                    ee_export_image_zip(hand_img, 'hand_map.tif', scale=scale, region=roi)
            def dwn_lcr(_):
                with out:
                    ee_export_image_zip(lcr_img, 'lc_raw_map.tif', scale=scale, region=roi)
            
            def dwn_tc(_):
                with out:
                    ee_export_image_zip(tc_img, 'tc_map.tif', scale=scale, region=roi)
            def dwn_lc(_):
                with out:
                    ee_export_image_zip(lc_img, 'lc_map.tif', scale=scale, region=roi)
            def dwn_hlc(_):
                with out:
                    ee_export_image_zip(hlc_img, 'hlc_map.tif', scale=scale, region=roi)

            dem_button.on_click(dwn_dem)
            slp_button.on_click(dwn_slp)
            hand_button.on_click(dwn_hand)
            lcr_button.on_click(dwn_lcr)
            
            tc_button.on_click(dwn_tc)
            lc_button.on_click(dwn_lc)
            hlc_button.on_click(dwn_hlc)
            
            display(out)
            
            return
        
        roi = ee.Geometry.BBox(float(west), float(south), float(east), float(north))
        centroid = list(reversed(roi.centroid(1).getInfo()['coordinates']))    
        
        # Parameters
        lcy_range = widgets.IntSlider(min=start_y, max=2019, value=2010, description='LC classification year: ', style={'description_width': 'initial'})
        slope_range = widgets.IntSlider(min=1, max=30, value=10,         description='Slope threshold:        ', style={'description_width': 'initial'})
        hand_range = widgets.IntSlider(min=1, max=30, value=10,          description='HAND threshold:         ', style={'description_width': 'initial'})
        param_ops = interactive(params, #{'manual': True},
                                Slope_th = slope_range,
                                HAND_th = hand_range,
                                LC_year = lcy_range)
        print('\nSet parameters for map computation:')
        display(param_ops)        
        
        return
    
    scale = float(Scale)
    dem_img = DEM
    slp_img = ee.Terrain.slope(dem_img).divide(180).multiply(ee.Number(np.pi)).tan().multiply(100)
    hand_img = HAND#.reproject(crs='EPSG:4326', scale=scale)
    lc_imgcol = LandCover

    if lc_imgcol == mapbiomas:
        start_y = 1985
    else:
        start_y = 2001
    
    # Region Of Interest    
    west = widgets.Text(description='West limit:', value='-55', layout=Layout(width='150px', grid_area='west'))
    south = widgets.Text(description='South limit:', value='-15', layout=Layout(width='150px', grid_area='south'))
    east = widgets.Text(description='East limit:', value='-51', layout=Layout(width='150px', grid_area='east'))
    north = widgets.Text(description='North limit:', value='-11', layout=Layout(width='150px', grid_area='north'));
    bbox_coords = widgets.GridBox(children=[north, west, east, south],
                layout=Layout(
                width='50%',
                grid_template_rows='auto auto auto',
                grid_template_columns='auto auto auto',
                grid_template_areas='''
                ". north ."
                "west . east"
                ". south ."
                '''))    
    output = interactive_output(roi_from_coords, {'west': west, 'south': south, 'east': east, 'north': north})
    
    print('\nDelimit extension of region to download data:')
    display(bbox_coords, output)
    
    return

#folder = widgets.Text(description='Folder', value='D:/')#get_download_folder())

# Maps
demList = {'MERIT-DEM 90m': ee.Image("MERIT/DEM/v1_0_3").select('dem'),
           'SRTM-DEM V4 90m': ee.Image("CGIAR/SRTM90_V4").select('elevation'),
           'SRTM-DEM  V3 30m': ee.Image("USGS/SRTMGL1_003").select('elevation'),
           'NASA-DEM 30m': ee.Image("NASA/NASADEM_HGT/001").select('elevation')}          
handList ={'TPS 90m (South America only)': ee.Image("projects/ee-rbfontana/assets/HAND_SA_merit_90m"),
           'FA=8km² 90m': ee.Image("users/gena/GlobalHAND/90m-global/hand-1000"),
           'FA=5km² 30m': ee.Image("users/gena/GlobalHAND/30m/hand-5000"),
           'FA=1km² 30m': ee.Image("users/gena/GlobalHAND/30m/hand-1000")}
lcList = {'Mapbiomas 30m (Brazil only)': ee.Image('projects/mapbiomas-workspace/public/collection5/mapbiomas_collection50_integration_v1'),
          'MODIS IGBP 500m': ee.ImageCollection("MODIS/006/MCD12Q1").select('LC_Type1'),
          'MODIS UMD 500m': ee.ImageCollection("MODIS/006/MCD12Q1").select('LC_Type2'),
          'MODIS LAI 500m': ee.ImageCollection("MODIS/006/MCD12Q1").select('LC_Type3'),
          'MODIS PTT 500m': ee.ImageCollection("MODIS/006/MCD12Q1").select('LC_Type5')}

mapbiomas = list(lcList.values())[0]
igbp = list(lcList.values())[1]
umd = list(lcList.values())[2]
lai = list(lcList.values())[3]
pft = list(lcList.values())[4]


demDdwn = widgets.Dropdown(options=demList, description='DEM: ')
handDdwn = widgets.Dropdown(options=handList, description='HAND: ')
lcDdwn = widgets.Dropdown(options=lcList, description='Land Cover: ')
scaleTxt = widgets.Text(description='Scale (m): ', value='90')
input_ops = interactive(inputs,
                        #folder=folder,
                        DEM=demDdwn,
                        HAND=handDdwn,
                        LandCover=lcDdwn,
                        Scale=scaleTxt)

print('Select map atributes:')
display(input_ops)<jupyter_output>Select map atributes:
{'fraction_non_alphanumeric': 0.12413793103448276, 'fraction_numerical': 0.033693077125559356, 'mean_word_length': 2.0152380952380953, 'pattern_counts': {'":': 14, '<': 9, '<?xml version=': 0, '>': 11, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 44, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '1', 'dir': 'jupyter-structured-clean-dedup', 'id': '471935', 'license': 'MIT', 'n_tokens_mistral': 6496, 'n_tokens_neox': 5848, 'n_words': 1482, 'path': 'HLC_map_download.ipynb', 'repo_name': 'rbfont/hlc-generator', 'size': '19029'}