diff --git a/.env.example b/.env.example index d8190878..6e262998 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,9 @@ DB_PASSWORD=xxxxxxx # Django settings DEBUG=xxxxxxx SECRET_KEY=xxxxxxx +LAYER_GENERATION_SYNC_MODE=False +SYNC_LAYER=False +STAC_UPLOAD_TO_S3=False USERNAME_GESDISC=xxxxxxx PASSWORD_GESDISC=xxxxxxx @@ -43,6 +46,8 @@ GEE_SERVICE_ACCOUNT_KEY_PATH=xxxxxxx GEE_HELPER_SERVICE_ACCOUNT_KEY_PATH=xxxxxxx GEE_SERVICE_ACCOUNT_KEY_NRM_WORK_PATH=xxxxxxx GEE_DATASETS_SERVICE_ACCOUNT_KEY_PATH=xxxx +GEE_STORAGE_PROJECT=ee-corestackdev +GEE_STORAGE_PROJECT_HELPER=core-stack-dev-2 FERNET_KEY=xxx # Misc diff --git a/.gitignore b/.gitignore index 65411952..ad161aa2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ venv/ *.pyc .idea/ .DS_Store -.cursor/ +.installation_state/ # Django *.log @@ -48,3 +48,11 @@ users/migrations/*.py */migrations/*.py */migrations/__pycache__/ + + +# ai +AGENTS.md +CLAUDE.md +.codex/ +.cursor/ +.ruff_cache diff --git a/.installation_state/collectstatic.done b/.installation_state/collectstatic.done new file mode 100644 index 00000000..2812016d --- /dev/null +++ b/.installation_state/collectstatic.done @@ -0,0 +1 @@ +2026-04-13T19:43:46Z diff --git a/.installation_state/conda_env.done b/.installation_state/conda_env.done new file mode 100644 index 00000000..60785b45 --- /dev/null +++ b/.installation_state/conda_env.done @@ -0,0 +1 @@ +2026-04-13T19:43:39Z diff --git a/.installation_state/env_file.done b/.installation_state/env_file.done new file mode 100644 index 00000000..278c3835 --- /dev/null +++ b/.installation_state/env_file.done @@ -0,0 +1 @@ +2026-04-13T19:43:40Z diff --git a/.installation_state/miniconda.done b/.installation_state/miniconda.done new file mode 100644 index 00000000..e0463ce4 --- /dev/null +++ b/.installation_state/miniconda.done @@ -0,0 +1 @@ +2026-04-13T19:40:19Z diff --git a/.installation_state/postgres.done b/.installation_state/postgres.done new file mode 100644 index 00000000..c4573478 --- /dev/null +++ b/.installation_state/postgres.done @@ -0,0 +1 @@ +2026-04-13T19:40:33Z diff --git a/.installation_state/rabbitmq.done b/.installation_state/rabbitmq.done new file mode 100644 index 00000000..d868a03c --- /dev/null +++ b/.installation_state/rabbitmq.done @@ -0,0 +1 @@ +2026-04-13T19:40:34Z diff --git a/.installation_state/unzip_install.done b/.installation_state/unzip_install.done new file mode 100644 index 00000000..8fe01fb6 --- /dev/null +++ b/.installation_state/unzip_install.done @@ -0,0 +1 @@ +2026-04-13T19:40:06Z diff --git a/README.md b/README.md index 42054b77..001a4cc9 100644 --- a/README.md +++ b/README.md @@ -43,16 +43,41 @@ chmod +x install.sh > For any installation issues, check [Installation Documentation](https://docs.core-stack.org/developers/installer/) and [Troubleshooting Guide](https://docs.core-stack.org/developers/setup-troubleshooting/). -#### 3. Running the server -After the successfull installation of all the packages, run the following commands to start the Django server: -```bash -conda activate corestack-backend (or whatever is the name of your virtual environment) -python manage.py runserver -``` -- **Running celery:** -If you are running some tasks, you need to run -```bash -celery -A nrm_app worker -l info -Q nrm +#### 3. Running the server +After the successfull installation of all the packages, run the following commands to start the Django server: +```bash +conda activate corestack-backend (or whatever is the name of your virtual environment) +python manage.py runserver +``` + +#### Download base layers + +After installation, download the local base layers into `data/` before running local compute pipelines: + +```bash +conda activate corestack-backend +python manage.py local_compute_layer_setup +``` + +To inspect available layer selectors: + +```bash +python manage.py local_compute_layer_setup --list +``` + +To download only specific layers or groups: + +```bash +python manage.py local_compute_layer_setup terrain mws lulc_v3 +python manage.py local_compute_layer_setup static_layers +python manage.py local_compute_layer_setup tehsil_level +python manage.py local_compute_layer_setup --ensure-soi-tehsil --ensure-tehsil-watersheds +``` + +- **Running celery:** +If you are running some tasks, you need to run +```bash +celery -A nrm_app worker -l info -Q nrm ``` where 'nrm_app' is the app_name and 'nrm' is the rabbitmq queue. diff --git a/computing/api.py b/computing/api.py index 543e4a6c..6639ea8e 100644 --- a/computing/api.py +++ b/computing/api.py @@ -1,8 +1,12 @@ import json +import inspect +import logging import os + import requests -from computing.forest_fire.forest_fire_updated import generate_forest_fire_layer_updated -from nrm_app.settings import BASE_DIR, LOCAL_COMPUTE_API_URL +from django.conf import settings +from django.core.files.storage import FileSystemStorage +from rest_framework import status from rest_framework.decorators import ( api_view, authentication_classes, @@ -10,67 +14,228 @@ permission_classes, schema, ) +from rest_framework.parsers import MultiPartParser, FormParser from rest_framework.permissions import AllowAny from rest_framework.response import Response -from rest_framework import status -from rest_framework.parsers import MultiPartParser, FormParser +from computing.STAC_specs.stac_collection import STACConfig, sanitize_text +from computing.change_detection.change_detection import ( + get_change_detection as get_change_detection_gee_task, +) +from computing.change_detection.change_detection_local import ( + get_change_detection as get_change_detection_local_task, +) from computing.change_detection.change_detection_vector import ( - vectorise_change_detection, + vectorise_change_detection as vectorise_change_detection_gee_task, +) +from computing.change_detection.change_detection_vector_local import ( + vectorise_change_detection as vectorise_change_detection_local_task, +) +from computing.layer_dependency.layer_generation_in_order import ( + layer_generate_map, + normalize_compute as _normalize_layer_order_compute, + validate_layer_map_request, +) +from computing.misc.drainage_lines import ( + clip_drainage_lines as clip_drainage_lines_gee_task, +) +from computing.misc.drainage_lines_local_compute import ( + clip_drainage_lines as clip_drainage_lines_local_task, +) +from computing.STAC_specs.stac_collection import STACConfig, sanitize_text +from nrm_app.settings import BASE_DIR, LOCAL_COMPUTE_API_URL +from utilities.auth_check_decorator import api_security_check +from utilities.constants import KML_PATH +from utilities.gee_utils import check_gee_task_status, download_gee_layer +from utilities.pipelines import api_request_payload +from utilities.layer_generation_mode import ( + sync_layer_generation_if_enabled, +) +from utilities.layer_generation_logging import ( + layer_generation_api_logging, +) +from .STAC_specs.stac_collection import generate_stac_collection_task +from .clart.clart import generate_clart_layer +from .clart.fes_clart_to_geoserver import generate_fes_clart_layer +from .crop_grid.crop_grid import create_crop_grids +from .cropping_intensity.cropping_intensity import generate_cropping_intensity +from .cropping_intensity.cropping_intesity_local import ( + generate_cropping_intensity as generate_cropping_intensity_local_task, ) +from .forest_fire.forest_fire import generate_forest_fire_layer +from .soil_health.soil_health import soil_health_local +from .soil_type.soil_type_local import generate_soil_type_local + +from .drought.drought import calculate_drought +from .drought.drought_causality import drought_causality from .et_downscale.et_downscale import generate_et_downscale -from .utils import ( - save_layer_info_to_db, - update_layer_sync_status, +from .forest_fringe.forest_fringe import generate_forest_fringe_degradation +from .local_compute_helper import ( + get_compute_mode as _get_compute_mode, ) -from django.conf import settings -from computing.STAC_specs.stac_collection import sanitize_text, STACConfig -from .lulc.lulc_vector import vectorise_lulc +from .local_compute_helper import ( + select_compute_task as _select_compute_task, +) +from .lulc.lulc_v3 import clip_lulc_v3 as clip_lulc_v3_gee_task +from .lulc.lulc_v3_local import clip_lulc_v3 as clip_lulc_v3_local_task +from .lulc.lulc_vector import vectorise_lulc as vectorise_lulc_gee_task +from .lulc.lulc_vector_local import vectorise_lulc as vectorise_lulc_local_task from .lulc.river_basin_lulc.lulc_v2_river_basin import lulc_river_basin_v2 from .lulc.river_basin_lulc.lulc_v3_river_basin_using_v2 import lulc_river_basin_v3 from .lulc.tehsil_level.lulc_v2 import generate_lulc_v2_tehsil from .lulc.tehsil_level.lulc_v3 import generate_lulc_v3_tehsil from .lulc.v4.lulc_v4 import generate_lulc_v4 +from .lulc_X_terrain.lulc_on_plain_cluster import ( + lulc_on_plain_cluster as lulc_on_plain_cluster_gee_task, +) +from .lulc_X_terrain.lulc_on_plain_cluster_local import ( + lulc_on_plain_cluster_local as lulc_on_plain_cluster_local_task, +) +from .lulc_X_terrain.lulc_on_slope_cluster import ( + lulc_on_slope_cluster as lulc_on_slope_cluster_gee_task, +) +from .lulc_X_terrain.lulc_on_slope_cluster_local import ( + lulc_on_slope_cluster_local as lulc_on_slope_cluster_local_task, +) +from .misc.admin_boundary import generate_tehsil_shape_file_data +from .misc.agroecological_space import generate_agroecological_data +from .misc.agroecological_space_local_compute import ( + generate_agroecological_data_local as generate_agroecological_data_local_task, +) +from .misc.antyodaya import generate_antyodaya_layer_task +from .misc.aquifer_vector import ( + generate_aquifer_vector as generate_aquifer_vector_gee_task, +) +from .misc.aquifer_vector_local import ( + generate_aquifer_vector as generate_aquifer_vector_local_task, +) +from .misc.canal_layer import canal_vector +from .misc.canal_local_compute import canal_vector as canal_vector_local_task +from .misc.catchment_area import generate_catchment_area_singleflow +from .misc.catchment_area_local_compute import ( + generate_catchment_area_singleflow_local as generate_catchment_area_singleflow_local_task, +) +from .misc.digital_elevation_model import generate_dem_layer +from .misc.digital_elevation_model_local import ( + generate_febdem_raster_vector_clip as generate_febdem_raster_vector_clip_local_task, +) +from .misc.distancetonearestdrainage import generate_distance_to_nearest_drainage_line +from .misc.distancetonearestdrainage_local_compute import ( + generate_distance_to_nearest_drainage_line_local as generate_distance_to_nearest_drainage_line_local_task, +) +from .misc.drainage_density_local_compute import ( + drainage_density as drainage_density_vector_local_task, +) +from .misc.facilities.pipeline import generate_facilities_proximity_task +from .misc.factory_csr import generate_factory_csr_data +from .misc.factory_csr_local_compute import ( + generate_factory_csr_data_local as generate_factory_csr_data_local_task, +) +from .misc.green_credit import generate_green_credit_data +from .misc.green_credit_local_compute import ( + generate_green_credit_data_local as generate_green_credit_data_local_task, +) +from .misc.lcw_conflict import generate_lcw_conflict_data +from .misc.lcw_conflict_local_compute import ( + generate_lcw_conflict_data_local as generate_lcw_conflict_data_local_task, +) +from .misc.livestocks.pipeline import generate_livestocks_layer_task +from .misc.mining_data import generate_mining_data +from .misc.mining_data_local_compute import ( + generate_mining_data_local as generate_mining_data_local_task, +) +from .misc.naturaldepression import generate_natural_depression_data +from .misc.naturaldepression_local_compute import ( + generate_natural_depression_data_local as generate_natural_depression_data_local_task, +) from .misc.ndvi_time_series import ndvi_timeseries +from .misc.nrega import clip_nrega_district_block +from .misc.nrega_local_compute import ( + generate_nrega_data_local as generate_nrega_data_local_task, +) from .misc.restoration_opportunity import generate_restoration_opportunity +from .misc.restoration_opportunity_local_compute import ( + generate_restoration_opportunity_local as generate_restoration_opportunity_local_task, +) +from .misc.river_local_compute import river_vector as river_vector_local_task +from .misc.slope_percentage import generate_slope_percentage_data +from .misc.slope_percentage_local_compute import ( + generate_slope_percentage_data_local as generate_slope_percentage_data_local_task, +) +from .misc.soge_vector import generate_soge_vector +from .misc.soge_vector_local_compute import ( + generate_soge_vector_local as generate_soge_vector_local_task, +) from .misc.stream_order import generate_stream_order from .mws.generate_hydrology import generate_hydrology +from .mws.mws import mws_layer +from .mws.mws_centroid import generate_mws_centroid_data +from .mws.mws_centroid_local_compute import ( + generate_mws_centroid_data_local as generate_mws_centroid_data_local_task, +) +from .mws.mws_connectivity import ( + generate_mws_connectivity_data as generate_mws_connectivity_gee_task, +) +from .mws.mws_connectivity_local_compute import ( + mws_connectivity_vector as generate_mws_connectivity_local_task, +) +from .plantation.site_suitability import site_suitability +from .spei.spei import ( + generate_spei_pipeline, + run_drought_resistance_resilience, + run_rainfall_resistance_resilience, + run_forest_fire_resistance_resilience, + run_high_wind_resistance_resilience, +) +from .surface_water_bodies.merge_swb_ponds import merge_swb_ponds +from .surface_water_bodies.swb import generate_swb_layer as generate_swb_gee_task +from .surface_water_bodies.swb_local import ( + generate_swb_layer as generate_swb_local_task, +) +from .terrain_descriptor.terrain_clusters import ( + generate_terrain_clusters as generate_terrain_clusters_gee_task, +) +from .terrain_descriptor.terrain_clusters_local import ( + generate_terrain_clusters as generate_terrain_clusters_local_task, +) +from .terrain_descriptor.terrain_compute_all_local import ( + generate_terrain_compute_all as generate_terrain_compute_all_task, +) +from .terrain_descriptor.terrain_raster_fabdem import ( + generate_terrain_raster_clip as generate_terrain_raster_clip_gee_task, +) +from .terrain_descriptor.terrain_raster_fabdem_local import ( + generate_terrain_raster_clip as generate_terrain_raster_clip_local_task, +) +from .tree_health.gee.canopy_height import tree_health_ch_raster +from .tree_health.gee.canopy_height_vector import tree_health_ch_vector +from .tree_health.gee.ccd import tree_health_ccd_raster +from .tree_health.gee.ccd_vector import tree_health_ccd_vector +from .tree_health.gee.overall_change import tree_health_overall_change_raster +from .tree_health.gee.overall_change_vector import tree_health_overall_change_vector +from .tree_health.local.canopy_height_local import tree_health_ch_raster_local +from .tree_health.local.canopy_height_vector_local import tree_health_ch_vector_local +from .tree_health.local.ccd_local import tree_health_ccd_raster_local +from .tree_health.local.ccd_vector_local import tree_health_ccd_vector_local + +from .tree_health.ltp_stp.generate_ltp_stp_change_local import ( + generate_ltp_stp_change_local, +) +from .tree_health.ltp_stp.generate_ltp_stp_local import generate_ltp_stp_local + +from .tree_health.local.overall_change_local import ( + tree_health_overall_change_raster_local, +) +from .tree_health.local.overall_change_vector_local import ( + tree_health_overall_change_vector_local, +) +from .tree_in_grassland.tree_in_grassland import generate_tree_in_grassland_layer from .utils import ( Geoserver, kml_to_shp, + save_layer_info_to_db, + update_layer_sync_status, ) -from utilities.gee_utils import download_gee_layer, check_gee_task_status -from django.core.files.storage import FileSystemStorage -from utilities.constants import KML_PATH -from .mws.mws import mws_layer -from .cropping_intensity.cropping_intensity import generate_cropping_intensity -from .surface_water_bodies.swb import generate_swb_layer -from .drought.drought import calculate_drought -from .terrain_descriptor.terrain_clusters import generate_terrain_clusters -from .terrain_descriptor.terrain_raster_fabdem import generate_terrain_raster_clip -from computing.misc.drainage_lines import clip_drainage_lines -from .lulc_X_terrain.lulc_on_slope_cluster import lulc_on_slope_cluster -from .lulc_X_terrain.lulc_on_plain_cluster import lulc_on_plain_cluster -from .clart.clart import generate_clart_layer -from .misc.admin_boundary import generate_tehsil_shape_file_data -from .misc.nrega import clip_nrega_district_block -from computing.change_detection.change_detection import get_change_detection -from .lulc.lulc_v3 import clip_lulc_v3 -from .crop_grid.crop_grid import create_crop_grids -from .tree_health.ccd import tree_health_ccd_raster -from .tree_health.canopy_height import tree_health_ch_raster -from .tree_health.overall_change import tree_health_overall_change_raster -from .drought.drought_causality import drought_causality -from .tree_health.overall_change_vector import tree_health_overall_change_vector -from .tree_health.canopy_height_vector import tree_health_ch_vector -from .tree_health.ccd_vector import tree_health_ccd_vector -from .plantation.site_suitability import site_suitability -from .misc.aquifer_vector import generate_aquifer_vector -from .misc.soge_vector import generate_soge_vector -from .clart.fes_clart_to_geoserver import generate_fes_clart_layer -from .surface_water_bodies.merge_swb_ponds import merge_swb_ponds -from utilities.auth_check_decorator import api_security_check -from computing.layer_dependency.layer_generation_in_order import layer_generate_map from .views import ( layer_status, get_layers_of_workspace, @@ -78,27 +243,9 @@ clear_layer_cache, check_missing_excel_files, ) -from .misc.lcw_conflict import generate_lcw_conflict_data -from .misc.agroecological_space import generate_agroecological_data -from .misc.factory_csr import generate_factory_csr_data -from .misc.green_credit import generate_green_credit_data -from .misc.mining_data import generate_mining_data -from .misc.slope_percentage import generate_slope_percentage_data -from .misc.naturaldepression import generate_natural_depression_data -from .misc.distancetonearestdrainage import generate_distance_to_nearest_drainage_line -from .misc.catchment_area import generate_catchment_area_singleflow from .zoi_layers.zoi import generate_zoi -from .mws.mws_connectivity import generate_mws_connectivity_data -from .mws.mws_centroid import generate_mws_centroid_data -from .misc.facilities import generate_facilities_proximity_task -from utilities.pipelines import api_request_payload -from .misc.antyodaya import generate_antyodaya_layer_task -from .misc.livestocks import generate_livestocks_layer_task -from .misc.digital_elevation_model import generate_dem_layer -from .misc.canal_layer import canal_vector -from .STAC_specs.stac_collection import generate_stac_collection_task -from .tree_in_grassland.tree_in_grassland import generate_tree_in_grassland_layer -from .forest_fringe.forest_fringe import generate_forest_fringe_degradation + +logger = logging.getLogger(__name__) @api_security_check(allowed_methods="POST") @@ -130,8 +277,20 @@ def generate_nrega_layer(request): district = request.data.get("district").lower() block = request.data.get("block").lower() gee_account_id = request.data.get("gee_account_id") - clip_nrega_district_block.apply_async( - args=[state, district, block, gee_account_id], queue="nrm" + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + clip_nrega_district_block, + generate_nrega_data_local_task, + ) + task.apply_async( + kwargs={ + "state": state, + "district": district, + "block": block, + "gee_account_id": gee_account_id, + }, + queue="nrm", ) return Response( {"Success": "Successfully initiated"}, status=status.HTTP_200_OK @@ -150,7 +309,13 @@ def generate_drainage_layer(request): district = request.data.get("district").lower() block = request.data.get("block").lower() gee_account_id = request.data.get("gee_account_id") - clip_drainage_lines.apply_async( + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + clip_drainage_lines_gee_task, + clip_drainage_lines_local_task, + ) + task.apply_async( kwargs={ "state": state, "district": district, @@ -162,6 +327,9 @@ def generate_drainage_layer(request): return Response( {"Success": "Successfully initiated"}, status=status.HTTP_200_OK ) + except ValueError as e: + print("Invalid request in generate_drainage_layer api :: ", e) + return Response({"Exception": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: print("Exception in generate_drainage_layer api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -406,13 +574,22 @@ def lulc_v3(request): start_year = request.data.get("start_year") end_year = request.data.get("end_year") gee_account_id = request.data.get("gee_account_id") - clip_lulc_v3.apply_async( + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + clip_lulc_v3_gee_task, + clip_lulc_v3_local_task, + ) + task.apply_async( args=[state, district, block, start_year, end_year, gee_account_id], queue="nrm", ) return Response( {"Success": "LULC v3 task initiated"}, status=status.HTTP_200_OK ) + except ValueError as e: + print("Invalid request in lulc_v3 api :: ", e) + return Response({"Exception": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: print("Exception in lulc_v3 api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -429,7 +606,13 @@ def lulc_vector(request): start_year = request.data.get("start_year") end_year = request.data.get("end_year") gee_account_id = request.data.get("gee_account_id") - vectorise_lulc.apply_async( + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + vectorise_lulc_gee_task, + vectorise_lulc_local_task, + ) + task.apply_async( args=[state, district, block, start_year, end_year, gee_account_id], queue="nrm", ) @@ -437,6 +620,9 @@ def lulc_vector(request): {"Success": "lulc_vector task initiated"}, status=status.HTTP_200_OK, ) + except ValueError as e: + print("Invalid request in lulc_vector api :: ", e) + return Response({"Exception": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: print("Exception in lulc_vector api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -493,7 +679,13 @@ def generate_ci_layer(request): start_year = request.data.get("start_year") end_year = request.data.get("end_year") gee_account_id = request.data.get("gee_account_id") - generate_cropping_intensity.apply_async( + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_cropping_intensity, + generate_cropping_intensity_local_task, + ) + task.apply_async( kwargs={ "state": state, "district": district, @@ -508,6 +700,9 @@ def generate_ci_layer(request): {"Success": "Cropping Intensity task initiated"}, status=status.HTTP_200_OK, ) + except ValueError as e: + print("Invalid request in generate_cropping_intensity_layer api :: ", e) + return Response({"Exception": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: print("Exception in generate_cropping_intensity_layer api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -521,23 +716,41 @@ def generate_swb(request): state = request.data.get("state") district = request.data.get("district") block = request.data.get("block") + roi = request.data.get("roi") + roi_path = request.data.get("roi_path") + asset_suffix = request.data.get("asset_suffix") start_year = request.data.get("start_year") end_year = request.data.get("end_year") gee_account_id = request.data.get("gee_account_id") - generate_swb_layer.apply_async( - kwargs={ - "state": state, - "district": district, - "block": block, - "start_year": start_year, - "end_year": end_year, - "gee_account_id": gee_account_id, - }, - queue="nrm", - ) + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_swb_gee_task, + generate_swb_local_task, + ) + task_kwargs = { + "state": state, + "district": district, + "block": block, + "start_year": start_year, + "end_year": end_year, + "gee_account_id": gee_account_id, + } + if compute == "local": + task_kwargs.update( + { + "roi": roi, + "roi_path": roi_path, + "asset_suffix": asset_suffix, + } + ) + task.apply_async(kwargs=task_kwargs, queue="nrm") return Response( {"Success": "Generate swb task initiated"}, status=status.HTTP_200_OK ) + except ValueError as e: + print("Invalid request in generate_swf api :: ", e) + return Response({"Exception": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: print("Exception in generate_swf api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -583,18 +796,56 @@ def generate_terrain_descriptor(request): district = request.data.get("district") block = request.data.get("block") gee_account_id = request.data.get("gee_account_id") - generate_terrain_clusters.apply_async( - args=[state, district, block, gee_account_id], queue="nrm" + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_terrain_clusters_gee_task, + generate_terrain_clusters_local_task, ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") return Response( {"Success": "generate_terrain_descriptor task initiated"}, status=status.HTTP_200_OK, ) + except ValueError as e: + print("Invalid request in generate_terrain_descriptor api :: ", e) + return Response({"Exception": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: print("Exception in generate_terrain_descriptor api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) +@api_view(["POST"]) +@schema(None) +def generate_terrain_compute_all(request): + print("Inside generate_terrain_compute_all") + try: + state = request.data.get("state") + district = request.data.get("district") + block = request.data.get("block") + if block is None or str(block).strip().lower() in {"", "null", "none"}: + raise ValueError( + "block is null. Please provide a block value for terrain compute-all." + ) + start_year = request.data.get("start_year") + end_year = request.data.get("end_year") + gee_account_id = request.data.get("gee_account_id") + generate_terrain_compute_all_task.apply_async( + args=[state, district, block, start_year, end_year, gee_account_id], + queue="nrm", + ) + return Response( + {"Success": "generate_terrain_compute_all task initiated"}, + status=status.HTTP_200_OK, + ) + except ValueError as e: + print("Invalid request in generate_terrain_compute_all api :: ", e) + return Response({"Exception": str(e)}, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + print("Exception in generate_terrain_compute_all api :: ", e) + return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @api_view(["POST"]) @schema(None) def generate_terrain_raster(request): @@ -604,7 +855,13 @@ def generate_terrain_raster(request): district = request.data.get("district") block = request.data.get("block") gee_account_id = request.data.get("gee_account_id") - generate_terrain_raster_clip.apply_async( + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_terrain_raster_clip_gee_task, + generate_terrain_raster_clip_local_task, + ) + task.apply_async( kwargs={ "state": state, "district": district, @@ -618,6 +875,9 @@ def generate_terrain_raster(request): {"Success": "generate_terrain_raster task initiated"}, status=status.HTTP_200_OK, ) + except ValueError as e: + print("Invalid request in generate_terrain_raster api :: ", e) + return Response({"Exception": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: print("Exception in generate_terrain_raster api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -631,10 +891,16 @@ def terrain_lulc_slope_cluster(request): state = request.data.get("state") district = request.data.get("district") block = request.data.get("block") - start_year = request.data.get("start_year") - end_year = request.data.get("end_year") + start_year = int(request.data.get("start_year")) + end_year = int(request.data.get("end_year")) gee_account_id = request.data.get("gee_account_id") - lulc_on_slope_cluster.apply_async( + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + lulc_on_slope_cluster_gee_task, + lulc_on_slope_cluster_local_task, + ) + task.apply_async( args=[state, district, block, start_year, end_year, gee_account_id], queue="nrm", ) @@ -642,6 +908,9 @@ def terrain_lulc_slope_cluster(request): {"Success": "terrain_lulc_slope_cluster task initiated"}, status=status.HTTP_200_OK, ) + except ValueError as e: + print("Invalid request in terrain_lulc_slope_cluster api :: ", e) + return Response({"Exception": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: print("Exception in terrain_lulc_slope_cluster api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -655,10 +924,16 @@ def terrain_lulc_plain_cluster(request): state = request.data.get("state") district = request.data.get("district") block = request.data.get("block") - start_year = request.data.get("start_year") - end_year = request.data.get("end_year") + start_year = int(request.data.get("start_year")) + end_year = int(request.data.get("end_year")) gee_account_id = request.data.get("gee_account_id") - lulc_on_plain_cluster.apply_async( + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + lulc_on_plain_cluster_gee_task, + lulc_on_plain_cluster_local_task, + ) + task.apply_async( args=[state, district, block, start_year, end_year, gee_account_id], queue="nrm", ) @@ -666,6 +941,9 @@ def terrain_lulc_plain_cluster(request): {"Success": "terrain_lulc_plain_cluster task initiated"}, status=status.HTTP_200_OK, ) + except ValueError as e: + print("Invalid request in terrain_lulc_plain_cluster api :: ", e) + return Response({"Exception": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: print("Exception in terrain_lulc_plain_cluster api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -700,10 +978,16 @@ def change_detection(request): state = request.data.get("state").lower() district = request.data.get("district").lower() block = request.data.get("block").lower() - start_year = request.data.get("start_year") - end_year = request.data.get("end_year") + start_year = int(request.data.get("start_year")) + end_year = int(request.data.get("end_year")) gee_account_id = request.data.get("gee_account_id") - get_change_detection.apply_async( + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + get_change_detection_gee_task, + get_change_detection_local_task, + ) + task.apply_async( args=[state, district, block, start_year, end_year, gee_account_id], queue="nrm", ) @@ -711,6 +995,9 @@ def change_detection(request): {"Success": "change_detection task initiated"}, status=status.HTTP_200_OK, ) + except ValueError as e: + print("Invalid request in change_detection api :: ", e) + return Response({"Exception": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: print("Exception in change_detection api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -724,10 +1011,17 @@ def change_detection_vector(request): state = request.data.get("state").lower() district = request.data.get("district").lower() block = request.data.get("block").lower() - start_year = request.data.get("start_year") - end_year = request.data.get("end_year") + start_year = int(request.data.get("start_year")) + end_year = int(request.data.get("end_year")) gee_account_id = request.data.get("gee_account_id") - vectorise_change_detection.apply_async( + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + vectorise_change_detection_gee_task, + vectorise_change_detection_local_task, + ) + print("What is task? ", task) + task.apply_async( args=[state, district, block, start_year, end_year, gee_account_id], queue="nrm", ) @@ -735,6 +1029,9 @@ def change_detection_vector(request): {"Success": "change_detection_vector task initiated"}, status=status.HTTP_200_OK, ) + except ValueError as e: + print("Invalid request in change_detection_vector api :: ", e) + return Response({"Exception": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: print("Exception in change_detection_vector api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -796,37 +1093,48 @@ def tree_health_raster(request): start_year = request.data.get("start_year") end_year = request.data.get("end_year") gee_account_id = request.data.get("gee_account_id") - tree_health_ccd_raster.apply_async( - kwargs={ - "state": state, - "district": district, - "block": block, - "start_year": start_year, - "end_year": end_year, - "gee_account_id": gee_account_id, - }, + + compute = _get_compute_mode(request) + task_kwargs = { + "state": state, + "district": district, + "block": block, + "start_year": start_year, + "end_year": end_year, + } + if not compute == "local": + task_kwargs.update( + { + "gee_account_id": gee_account_id, + } + ) + ccd_task = _select_compute_task( + compute, + tree_health_ccd_raster, + tree_health_ccd_raster_local, + ) + + ccd_task.apply_async( + kwargs=task_kwargs, queue="nrm", ) - tree_health_ch_raster.apply_async( - kwargs={ - "state": state, - "district": district, - "block": block, - "start_year": start_year, - "end_year": end_year, - "gee_account_id": gee_account_id, - }, + + ch_task = _select_compute_task( + compute, + tree_health_ch_raster, + tree_health_ch_raster_local, + ) + ch_task.apply_async( + kwargs=task_kwargs, queue="nrm", ) - tree_health_overall_change_raster.apply_async( - kwargs={ - "state": state, - "district": district, - "block": block, - "start_year": start_year, - "end_year": end_year, - "gee_account_id": gee_account_id, - }, + overall_task = _select_compute_task( + compute, + tree_health_overall_change_raster, + tree_health_overall_change_raster_local, + ) + overall_task.apply_async( + kwargs=task_kwargs, queue="nrm", ) @@ -851,39 +1159,66 @@ def tree_health_vector(request): end_year = request.data.get("end_year") gee_account_id = request.data.get("gee_account_id") - tree_health_ch_vector.apply_async( - kwargs={ - "state": state, - "district": district, - "block": block, - "start_year": start_year, - "end_year": end_year, - "gee_account_id": gee_account_id, - }, - queue="nrm", + compute = _get_compute_mode(request) + + task_kwargs = { + "state": state, + "district": district, + "block": block, + "start_year": start_year, + "end_year": end_year, + } + if not compute == "local": + task_kwargs.update( + { + "gee_account_id": gee_account_id, + } + ) + + ccd_task = _select_compute_task( + compute, + tree_health_ccd_vector, + tree_health_ccd_vector_local, ) + print("What is task? ", ccd_task) - tree_health_ccd_vector.apply_async( - kwargs={ - "state": state, - "district": district, - "block": block, - "start_year": start_year, - "end_year": end_year, - "gee_account_id": gee_account_id, - }, + ccd_task.apply_async( + kwargs=task_kwargs, queue="nrm", ) - tree_health_overall_change_vector.apply_async( - kwargs={ - "state": state, - "district": district, - "block": block, - "gee_account_id": gee_account_id, - }, + ch_task = _select_compute_task( + compute, + tree_health_ch_vector, + tree_health_ch_vector_local, + ) + print("What is task? ", ch_task) + + ch_task.apply_async( + kwargs=task_kwargs, + queue="nrm", + ) + + overall_task = _select_compute_task( + compute, + tree_health_overall_change_vector, + tree_health_overall_change_vector_local, + ) + print("What is task? ", overall_task) + + task_kwargs = {"state": state, "district": district, "block": block} + if not compute == "local": + task_kwargs.update( + { + "gee_account_id": gee_account_id, + } + ) + + overall_task.apply_async( + kwargs=task_kwargs, queue="nrm", ) + return Response( {"Success": "Overall_change_vector task initiated"}, status=status.HTTP_200_OK, @@ -936,9 +1271,13 @@ def restoration_opportunity(request): district = request.data.get("district").lower() block = request.data.get("block").lower() gee_account_id = request.data.get("gee_account_id") - generate_restoration_opportunity.apply_async( - args=[state, district, block, gee_account_id], queue="nrm" + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_restoration_opportunity, + generate_restoration_opportunity_local_task, ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") return Response( {"Success": "restoration_opportunity task initiated"}, status=status.HTTP_200_OK, @@ -998,13 +1337,20 @@ def aquifer_vector(request): district = request.data.get("district").lower() block = request.data.get("block").lower() gee_account_id = request.data.get("gee_account_id") - generate_aquifer_vector.apply_async( - args=[state, district, block, gee_account_id], queue="nrm" + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_aquifer_vector_gee_task, + generate_aquifer_vector_local_task, ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") return Response( {"Success": "aquifer vector task initiated"}, status=status.HTTP_200_OK, ) + except ValueError as e: + print("Invalid request in aquifer vector api :: ", e) + return Response({"Exception": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: print("Exception in aquifer vector api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -1019,9 +1365,13 @@ def soge_vector(request): district = request.data.get("district").lower() block = request.data.get("block").lower() gee_account_id = request.data.get("gee_account_id") - generate_soge_vector.apply_async( - args=[state, district, block, gee_account_id], queue="nrm" + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_soge_vector, + generate_soge_vector_local_task, ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") return Response( {"Success": "SOGE vector task initiated"}, status=status.HTTP_200_OK, @@ -1050,7 +1400,7 @@ def fes_clart_upload_layer(request): # Save file to temp location file_extension = os.path.splitext(uploaded_file.name)[1] - filename = f'{district.strip().replace(" ", "_")}_{block.strip().replace(" ", "_")}_clart_fes{file_extension}' + filename = f"{district.strip().replace(' ', '_')}_{block.strip().replace(' ', '_')}_clart_fes{file_extension}" temp_upload_dir = os.path.join( BASE_DIR, @@ -1239,8 +1589,26 @@ def generate_layer_in_order(request): gee_account_id = request.data.get("gee_account_id") start_year = request.data.get("start_year") end_year = request.data.get("end_year") + compute = _normalize_layer_order_compute(request.data.get("compute") or "gee") start_year = int(start_year) if start_year is not None else None end_year = int(end_year) if end_year is not None else None + + logger.info( + f"generate_layer_in_order requested: state={state}, district={district}, " + f"block={block}, map={map_order}, compute={compute}" + ) + + validation_errors = validate_layer_map_request(map_order, compute=compute) + if validation_errors: + logger.error( + f"generate_layer_in_order validation failed for map={map_order}, " + f"compute={compute}: {'; '.join(validation_errors)}" + ) + return Response( + {"Exception": "; ".join(validation_errors)}, + status=status.HTTP_400_BAD_REQUEST, + ) + layer_generate_map.apply_async( kwargs={ "state": state, @@ -1250,15 +1618,21 @@ def generate_layer_in_order(request): "gee_account_id": gee_account_id, "start_year": start_year, "end_year": end_year, + "compute": compute, }, queue="nrm", ) return Response( {"Success": "Successfully initiated"}, status=status.HTTP_200_OK ) + except ValueError as e: + logger.warning(f"Invalid request in generate_layer_in_order api: {e}") + return Response({"Exception": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: - print("Exception in generate_layer_order_first api :: ", e) - return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + logger.exception("Exception in generate_layer_in_order api") + return Response( + {"Exception": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) @api_view(["POST"]) @@ -1275,8 +1649,10 @@ def layer_status_dashboard(request): status=status.HTTP_200_OK, ) except Exception as e: - print("Exception in layer_staus_dashboard api :: ", e) - return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + logger.exception("Exception in layer_status_dashboard api") + return Response( + {"Exception": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) @api_view(["POST"]) @@ -1288,9 +1664,13 @@ def generate_lcw(request): district = request.data.get("district").lower() block = request.data.get("block").lower() gee_account_id = request.data.get("gee_account_id") - generate_lcw_conflict_data.apply_async( - args=[state, district, block, gee_account_id], queue="nrm" + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_lcw_conflict_data, + generate_lcw_conflict_data_local_task, ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") return Response( {"Success": "Successfully initiated"}, status=status.HTTP_200_OK ) @@ -1308,9 +1688,13 @@ def generate_agroecological(request): district = request.data.get("district").lower() block = request.data.get("block").lower() gee_account_id = request.data.get("gee_account_id") - generate_agroecological_data.apply_async( - args=[state, district, block, gee_account_id], queue="nrm" + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_agroecological_data, + generate_agroecological_data_local_task, ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") return Response( {"Success": "Successfully initiated"}, status=status.HTTP_200_OK ) @@ -1328,9 +1712,13 @@ def generate_factory_csr(request): district = request.data.get("district").lower() block = request.data.get("block").lower() gee_account_id = request.data.get("gee_account_id") - generate_factory_csr_data.apply_async( - args=[state, district, block, gee_account_id], queue="nrm" + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_factory_csr_data, + generate_factory_csr_data_local_task, ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") return Response( {"Success": "Successfully initiated"}, status=status.HTTP_200_OK ) @@ -1348,9 +1736,13 @@ def generate_green_credit(request): district = request.data.get("district").lower() block = request.data.get("block").lower() gee_account_id = request.data.get("gee_account_id") - generate_green_credit_data.apply_async( - args=[state, district, block, gee_account_id], queue="nrm" + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_green_credit_data, + generate_green_credit_data_local_task, ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") return Response( {"Success": "Successfully initiated"}, status=status.HTTP_200_OK ) @@ -1368,9 +1760,13 @@ def generate_mining(request): district = request.data.get("district").lower() block = request.data.get("block").lower() gee_account_id = request.data.get("gee_account_id") - generate_mining_data.apply_async( - args=[state, district, block, gee_account_id], queue="nrm" + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_mining_data, + generate_mining_data_local_task, ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") return Response( {"Success": "Successfully initiated"}, status=status.HTTP_200_OK ) @@ -1401,9 +1797,13 @@ def generate_natural_depression(request): district = request.data.get("district").lower() block = request.data.get("block").lower() gee_account_id = request.data.get("gee_account_id") - generate_natural_depression_data.apply_async( - args=[state, district, block, gee_account_id], queue="nrm" + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_natural_depression_data, + generate_natural_depression_data_local_task, ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") return Response( {"Success": "Successfully initiated"}, status=status.HTTP_200_OK ) @@ -1421,9 +1821,13 @@ def generate_distance_nearest_upstream_DL(request): district = request.data.get("district").lower() block = request.data.get("block").lower() gee_account_id = request.data.get("gee_account_id") - generate_distance_to_nearest_drainage_line.apply_async( - args=[state, district, block, gee_account_id], queue="nrm" + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_distance_to_nearest_drainage_line, + generate_distance_to_nearest_drainage_line_local_task, ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") return Response( {"Success": "Successfully initiated"}, status=status.HTTP_200_OK ) @@ -1441,9 +1845,13 @@ def generate_catchment_area_SF(request): district = request.data.get("district").lower() block = request.data.get("block").lower() gee_account_id = request.data.get("gee_account_id") - generate_catchment_area_singleflow.apply_async( - args=[state, district, block, gee_account_id], queue="nrm" + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_catchment_area_singleflow, + generate_catchment_area_singleflow_local_task, ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") return Response( {"Success": "Successfully initiated"}, status=status.HTTP_200_OK ) @@ -1461,9 +1869,13 @@ def generate_slope_percentage(request): district = request.data.get("district").lower() block = request.data.get("block").lower() gee_account_id = request.data.get("gee_account_id") - generate_slope_percentage_data.apply_async( - args=[state, district, block, gee_account_id], queue="nrm" + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_slope_percentage_data, + generate_slope_percentage_data_local_task, ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") return Response( {"Success": "Successfully initiated"}, status=status.HTTP_200_OK ) @@ -1584,20 +1996,36 @@ def generate_zoi_to_gee(request): @api_view(["POST"]) @schema(None) def generate_mws_connectivity(request): - print("Inside generate_mws_connectivity_to_gee API.") + print("Inside generate_mws_connectivity API.") try: state = request.data.get("state").lower() district = request.data.get("district").lower() block = request.data.get("block").lower() gee_account_id = request.data.get("gee_account_id") - generate_mws_connectivity_data.apply_async( - args=[state, district, block, gee_account_id], queue="nrm" + + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_mws_connectivity_gee_task, + generate_mws_connectivity_local_task, + ) + task.apply_async( + kwargs={ + "state": state, + "district": district, + "block": block, + "gee_account_id": gee_account_id, + }, + queue="nrm", ) return Response( {"Success": "Successfully initiated"}, status=status.HTTP_200_OK ) + except ValueError as e: + print("Invalid request in generate_mws_connectivity api :: ", e) + return Response({"Exception": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: - print("Exception in generate_mws_connectivity_to_gee api :: ", e) + print("Exception in generate_mws_connectivity api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -1610,9 +2038,13 @@ def generate_mws_centroid(request): district = request.data.get("district").lower() block = request.data.get("block").lower() gee_account_id = request.data.get("gee_account_id") - generate_mws_centroid_data.apply_async( - args=[state, district, block, gee_account_id], queue="nrm" + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + generate_mws_centroid_data, + generate_mws_centroid_data_local_task, ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") return Response( {"Success": "Successfully initiated"}, status=status.HTTP_200_OK ) @@ -1627,7 +2059,11 @@ def generate_facilities_proximity(request): print("Inside generate_facilities_proximity API.") try: payload = api_request_payload( - request.data.dict() if hasattr(request.data, "dict") else dict(request.data), + ( + request.data.dict() + if hasattr(request.data, "dict") + else dict(request.data) + ), overwrite=True, ) generate_facilities_proximity_task.apply_async( @@ -1635,7 +2071,8 @@ def generate_facilities_proximity(request): queue="nrm", ) return Response( - {"Success": "Successfully initiated", "request": payload}, status=status.HTTP_200_OK + {"Success": "Successfully initiated", "request": payload}, + status=status.HTTP_200_OK, ) except ValueError as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) @@ -1650,15 +2087,20 @@ def generate_antyodaya(request): print("Inside generate_antyodaya API.") try: payload = api_request_payload( - request.data.dict() if hasattr(request.data, "dict") else dict(request.data), - overwrite=False, + ( + request.data.dict() + if hasattr(request.data, "dict") + else dict(request.data) + ), + overwrite=True, ) generate_antyodaya_layer_task.apply_async( kwargs={"payload": payload}, queue="nrm", ) return Response( - {"Success": "Successfully initiated", "request": payload}, status=status.HTTP_200_OK + {"Success": "Successfully initiated", "request": payload}, + status=status.HTTP_200_OK, ) except ValueError as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) @@ -1673,15 +2115,20 @@ def generate_livestocks(request): print("Inside generate_livestocks API.") try: payload = api_request_payload( - request.data.dict() if hasattr(request.data, "dict") else dict(request.data), - overwrite=False, + ( + request.data.dict() + if hasattr(request.data, "dict") + else dict(request.data) + ), + overwrite=True, ) generate_livestocks_layer_task.apply_async( kwargs={"payload": payload}, queue="nrm", ) return Response( - {"Success": "Successfully initiated", "request": payload}, status=status.HTTP_200_OK + {"Success": "Successfully initiated", "request": payload}, + status=status.HTTP_200_OK, ) except ValueError as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) @@ -1719,7 +2166,7 @@ def et_downscale(request): {"Success": "Successfully initiated"}, status=status.HTTP_200_OK ) except Exception as e: - print("Exception in generate_mws_centroid api :: ", e) + print("Exception in generate_et_downscale api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -1950,6 +2397,36 @@ def update_layer_sync_remote(request): @authentication_classes([]) @permission_classes([AllowAny]) @schema(None) +def update_layer_sync_remote(request): + """ + Called by a local compute instance to update sync/STAC flags on a layer + record in this (prod) backend. + """ + + api_key = getattr(settings, "PROD_BACKEND_API_KEY", "") + if api_key and request.headers.get("X-Api-Key") != api_key: + return Response({"error": "Unauthorized"}, status=status.HTTP_401_UNAUTHORIZED) + + try: + d = request.data + layer_id = d.get("layer_id") + if layer_id is None: + return Response( + {"error": "layer_id is required"}, status=status.HTTP_400_BAD_REQUEST + ) + + result = update_layer_sync_status( + layer_id=layer_id, + sync_to_geoserver=d.get("sync_to_geoserver"), + is_stac_specs_generated=d.get("is_stac_specs_generated"), + ) + return Response({"layer_id": result}, status=status.HTTP_200_OK) + except Exception as e: + return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@api_view(["POST"]) +@schema(None) def sync_layer_remote(request): """ Called by a local compute instance to persist a layer record on this (prod) backend. @@ -2025,10 +2502,30 @@ def generate_fabdem_layer(request): {"Success": "Successfully initiated"}, status=status.HTTP_200_OK ) except Exception as e: - print( - f"Exception in generate DEM raster and vector layer for {district} - {block}:: ", - e, + print(f"Exception in generate DEM raster and vector layer:", e) + return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@api_view(["POST"]) +@schema(None) +def generate_spei(request): + print("Inside generate_spei API.") + try: + aez = request.data.get("aez") + start_year = request.data.get("start_year") + end_year = request.data.get("end_year") + gee_account_id = request.data.get("gee_account_id") + overwrite = request.data.get("overwrite") or False + + generate_spei_pipeline.apply_async( + args=[aez, start_year, end_year, gee_account_id, overwrite], queue="nrm" ) + return Response( + {"Success": "Successfully initiated generate_spei task"}, + status=status.HTTP_200_OK, + ) + except Exception as e: + print("Exception in generate_spei api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -2048,9 +2545,218 @@ def generate_canal_vector(request): {"Success": "Successfully initiated"}, status=status.HTTP_200_OK ) except Exception as e: - print( - f"Exception in generate canal vector layer for {district} - {block}:: ", e + print(f"Exception in generate canal vector layer: ", e) + return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@api_view(["POST"]) +@schema(None) +def drought_resilience_resistance(request): + print("Inside drought_resilience_resistance API.") + try: + aez = request.data.get("aez") + start_year = request.data.get("start_year") + end_year = request.data.get("end_year") + gee_account_id = request.data.get("gee_account_id") + + run_drought_resistance_resilience.apply_async( + args=[aez, start_year, end_year, gee_account_id], queue="nrm" + ) + return Response( + { + "Success": "Successfully drought_resilience_resistance generate_spei task" + }, + status=status.HTTP_200_OK, + ) + except Exception as e: + print("Exception in drought_resilience_resistance api :: ", e) + return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@api_view(["POST"]) +@schema(None) +def rainfall_resilience_resistance(request): + print("Inside rainfall_resilience_resistance API.") + try: + aez = request.data.get("aez") + start_year = request.data.get("start_year") + end_year = request.data.get("end_year") + gee_account_id = request.data.get("gee_account_id") + + run_rainfall_resistance_resilience.apply_async( + args=[aez, start_year, end_year, gee_account_id], queue="nrm" + ) + return Response( + { + "Success": "Successfully rainfall_resilience_resistance generate_spei task" + }, + status=status.HTTP_200_OK, + ) + except Exception as e: + print("Exception in rainfall_resilience_resistance api :: ", e) + return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@api_view(["POST"]) +@schema(None) +def forest_fire_resilience_resistance(request): + print("Inside forest_fire_resilience_resistance API.") + try: + aez = request.data.get("aez") + start_year = request.data.get("start_year") + end_year = request.data.get("end_year") + gee_account_id = request.data.get("gee_account_id") + + run_forest_fire_resistance_resilience.apply_async( + args=[aez, start_year, end_year, gee_account_id], queue="nrm" + ) + return Response( + {"Success": "Successfully forest_fire_resilience_resistance task"}, + status=status.HTTP_200_OK, + ) + except Exception as e: + print("Exception in forest_fire_resilience_resistance api :: ", e) + return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@api_view(["POST"]) +@schema(None) +def high_wind_resilience_resistance(request): + print("Inside run_high_wind_resistance_resilience API.") + try: + aez = request.data.get("aez") + start_year = request.data.get("start_year") + end_year = request.data.get("end_year") + gee_account_id = request.data.get("gee_account_id") + + run_high_wind_resistance_resilience.apply_async( + args=[aez, start_year, end_year, gee_account_id], queue="nrm" + ) + return Response( + {"Success": "Successfully run_high_wind_resistance_resilience task"}, + status=status.HTTP_200_OK, + ) + except Exception as e: + print("Exception in run_high_wind_resistance_resilience api :: ", e) + return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@api_view(["POST"]) +@schema(None) +def generate_fabdem_raster_vector(request): + print("Inside generate DEM raster layer API.") + try: + state = request.data.get("state").lower() + district = request.data.get("district").lower() + block = request.data.get("block").lower() + gee_account_id = request.data.get("gee_account_id") + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + None, + generate_febdem_raster_vector_clip_local_task, + ) + if task is None: + return Response( + {"Error": "GEE execution not supported for this module."}, + status=status.HTTP_400_BAD_REQUEST, + ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") + return Response( + {"Success": "Successfully initiated"}, status=status.HTTP_200_OK + ) + except Exception as e: + print(f"Exception in generate DEM raster layer:", e) + return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@api_view(["POST"]) +@schema(None) +def generate_canal_vector(request): + print("Inside generate canal vector layer API.") + try: + state = request.data.get("state").lower() + district = request.data.get("district").lower() + block = request.data.get("block").lower() + gee_account_id = request.data.get("gee_account_id") + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + None, + canal_vector_local_task, + ) + if task is None: + return Response( + {"Error": "GEE execution not supported for this module."}, + status=status.HTTP_400_BAD_REQUEST, + ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") + return Response( + {"Success": f"Successfully initiated {compute} task"}, + status=status.HTTP_200_OK, + ) + except Exception as e: + print(f"Exception in generate canal vector layer: ", e) + return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@api_view(["POST"]) +@schema(None) +def generate_river_data(request): + print("Inside river data API.") + try: + state = request.data.get("state").lower() + district = request.data.get("district").lower() + block = request.data.get("block").lower() + gee_account_id = request.data.get("gee_account_id") + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + None, + river_vector_local_task, + ) + if task is None: + return Response( + {"Error": "GEE execution not supported for this module."}, + status=status.HTTP_400_BAD_REQUEST, + ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") + return Response( + {"Success": f"Successfully initiated {compute} task"}, + status=status.HTTP_200_OK, + ) + except Exception as e: + print("Exception in river data api :: ", e) + return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@api_view(["POST"]) +@schema(None) +def generate_drainage_density_data(request): + print("Inside river data API.") + try: + state = request.data.get("state").lower() + district = request.data.get("district").lower() + block = request.data.get("block").lower() + gee_account_id = request.data.get("gee_account_id") + compute = _get_compute_mode(request) + task = _select_compute_task( + compute, + None, + drainage_density_vector_local_task, + ) + if task is None: + return Response( + {"Error": "GEE execution not supported for this module."}, + status=status.HTTP_400_BAD_REQUEST, + ) + task.apply_async(args=[state, district, block, gee_account_id], queue="nrm") + return Response( + {"Success": f"Successfully initiated {compute} task"}, + status=status.HTTP_200_OK, ) + except Exception as e: + print("Exception in river data api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -2123,7 +2829,7 @@ def generate_forest_fire(request): start_year = request.data.get("start_year") end_year = request.data.get("end_year") gee_account_id = request.data.get("gee_account_id") - generate_forest_fire_layer_updated.apply_async( + generate_forest_fire_layer.apply_async( kwargs={ "state": state, "district": district, @@ -2152,3 +2858,135 @@ def missing_excel(request): except Exception as e: print("Exception in missing_excel api :: ", e) return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@api_view(["POST"]) +@schema(None) +def generate_soil_health(request): + print("Inside generate_soil_health API.") + try: + state = request.data.get("state").lower() + district = request.data.get("district").lower() + block = request.data.get("block").lower() + + soil_health_local.apply_async(args=[state, district, block], queue="nrm") + return Response( + {"Success": f"Successfully initiated generate_soil_health task"}, + status=status.HTTP_200_OK, + ) + except Exception as e: + print("Exception in generate_soil_health api :: ", e) + return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@api_view(["POST"]) +@schema(None) +def generate_soil_type(request): + try: + location = { + field: request.data.get(field) + for field in ("state", "district", "block") + } + compute = request.data.get("compute") + missing_fields = [field for field, value in location.items() if not value] + if not compute: + missing_fields.append("compute") + if missing_fields: + return Response( + {"Exception": f"Missing required fields: {', '.join(missing_fields)}"}, + status=status.HTTP_400_BAD_REQUEST, + ) + if compute.lower() != "local": + return Response( + {"Exception": "Soil type only supports compute=local"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + generate_soil_type_local.apply_async( + kwargs={field: value.lower() for field, value in location.items()}, + queue="nrm", + ) + return Response( + {"Success": "Successfully initiated generate_soil_type task"}, + status=status.HTTP_200_OK, + ) + except Exception as e: + logger.exception("Exception in generate_soil_type api") + return Response( + {"Exception": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + +@api_view(["POST"]) +@schema(None) +def generate_ltp_stp(request): + print("Inside generate_ltp_stp API.") + try: + start_year = request.data.get("start_year") + end_year = request.data.get("end_year") + + generate_ltp_stp_local.apply_async(args=[start_year, end_year], queue="nrm") + return Response( + {"Success": f"Successfully initiated generate_ltp_stp task"}, + status=status.HTTP_200_OK, + ) + except Exception as e: + print("Exception in generate_ltp_stp api :: ", e) + return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@api_view(["POST"]) +@schema(None) +def generate_ltp_stp_change(request): + print("Inside generate_ltp_stp API.") + try: + start_year = request.data.get("start_year") + end_year = request.data.get("end_year") + + generate_ltp_stp_change_local.apply_async( + args=[start_year, end_year], queue="nrm" + ) + return Response( + {"Success": f"Successfully initiated generate_ltp_stp task"}, + status=status.HTTP_200_OK, + ) + except Exception as e: + print("Exception in generate_ltp_stp api :: ", e) + return Response({"Exception": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +def _auto_discover_computing_api_views(namespace): + """Auto-wrap request handlers for sync layer generation + STAC enrichment.""" + discovered = [] + for name, fn in namespace.items(): + if name.startswith("_") or not callable(fn): + continue + if getattr(fn, "__module__", None) != __name__: + continue + if getattr(fn, "__layer_generation_sync_wrapped__", False): + continue + try: + target = inspect.unwrap(fn) + sig = inspect.signature(target) + except (OSError, TypeError, ValueError): + continue + + params = list(sig.parameters.values()) + if len(params) == 0: + continue + first_param = params[0] + if first_param.kind in ( + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + ) and first_param.name == "request": + discovered.append(name) + return discovered + + +for _view_name in _auto_discover_computing_api_views(globals()): + wrapped = sync_layer_generation_if_enabled( + layer_generation_api_logging(globals()[_view_name]) + ) + wrapped.__layer_generation_sync_wrapped__ = True + globals()[_view_name] = wrapped diff --git a/computing/base_layer_setup.py b/computing/base_layer_setup.py new file mode 100644 index 00000000..e15c6f9e --- /dev/null +++ b/computing/base_layer_setup.py @@ -0,0 +1,677 @@ +import logging +import subprocess +from functools import wraps +from inspect import signature +from pathlib import Path +from urllib.parse import urlparse + +import requests +import yaml + +from computing.config_loader import ( + ADMIN_BOUNDARY_INPUT_DIR, + ADMIN_BOUNDARY_OUTPUT_DIR, + DATA_DIR, + MICROWATERSHED_PATH, + PROJECT_ROOT, + SOI_TEHSIL_PATH, + VILLAGE_BOUNDARIES_DIR, +) +from computing.config_loader import ( + GDRIVE_ADMIN_BOUNDARY_FILE_ID as _GDRIVE_ADMIN_BOUNDARY_FILE_ID, +) +from computing.config_loader import ( + GDRIVE_MICROWATERSHED_FILE_ID as _GDRIVE_MICROWATERSHED_FILE_ID, +) +from computing.config_loader import ( + LULC_BASE_DIR as LULC_DIR, +) +from computing.config_loader import ( + LULC_GDRIVE_FILES as _LULC_GDRIVE_FILES, +) +from computing.config_loader import ( + PRECOMPUTED_TEHSIL_WATERSHED_DIR as TEHSIL_WATERSHEDS_DIR, +) +logger = logging.getLogger(__name__) + +CONFIG_NEW_PATH = Path(__file__).resolve().parent / "config.yaml" + +_SOI_WFS_PARAMS = { + "service": "WFS", + "version": "1.0.0", + "request": "GetFeature", + "typeName": "pan_india_asset:SOI_tehsil_pan_india_dataset", + "outputFormat": "application/json", +} + + +def _is_dir_populated(path: Path) -> bool: + return path.is_dir() and any(path.iterdir()) + + +def _layer_key(name: str) -> str: + return name.strip().lower().replace(" ", "_").replace("-", "_") + + +def _load_new_config() -> dict: + with open(CONFIG_NEW_PATH) as f: + return yaml.safe_load(f) or {} + + +def _local_path(rel_path: str) -> Path: + return Path(rel_path.replace("{DATA_DIR}", str(DATA_DIR))) + + +def _format_periodic_value(template: str, year: int) -> str: + return template.replace("{year+1}", str(year + 1)).replace("{year}", str(year)) + + +def _expand_periodic_layer(layer: dict) -> list[dict]: + if not layer.get("periodicity"): + return [layer] + + if layer.get("periodicity") != "annual": + raise ValueError( + f"Unsupported periodicity for base layer '{layer.get('name')}': " + f"{layer.get('periodicity')}" + ) + + expanded_layers = [] + for year in range(int(layer["start_year"]), int(layer["end_year"]) + 1): + filename = _format_periodic_value(layer["filename"], year) + expanded = dict(layer) + expanded["year"] = year + expanded["filename"] = filename + expanded["local_path"] = _format_periodic_value( + layer["local_path"].replace("{filename}", filename), year + ) + expanded["source"] = _format_periodic_value( + layer["source"].replace("{filename}", filename), year + ) + expanded_layers.append(expanded) + + return expanded_layers + + +def _expand_manifest_layers(layers: list[dict]) -> list[dict]: + expanded_layers = [] + for layer in layers: + expanded_layers.extend(_expand_periodic_layer(layer)) + return expanded_layers + + +def _manifest_group_key(path: tuple[str, ...]) -> str: + return "_".join(path) + + +def _walk_manifest_groups(node, path: tuple[str, ...] = ()): + if isinstance(node, list): + yield _manifest_group_key(path), _expand_manifest_layers(node), path + return + + if not isinstance(node, dict): + return + + descendant_layers = [] + for key, value in node.items(): + child_path = (*path, key) + if isinstance(value, list): + layers = _expand_manifest_layers(value) + descendant_layers.extend(layers) + yield _manifest_group_key(child_path), layers, child_path + elif isinstance(value, dict): + child_groups = list(_walk_manifest_groups(value, child_path)) + for group_name, layers, group_path in child_groups: + descendant_layers.extend(layers) + yield group_name, layers, group_path + + if path and descendant_layers: + yield _manifest_group_key(path), descendant_layers, path + + +def _manifest_layer_groups() -> dict[str, list[dict]]: + base_layers = _load_new_config().get("base_layers", {}) + groups = {} + leaf_aliases = {} + + for group_name, layers, group_path in _walk_manifest_groups(base_layers): + groups[group_name] = layers + leaf_name = group_path[-1] + leaf_aliases.setdefault(leaf_name, []).append(group_name) + + for leaf_name, group_names in leaf_aliases.items(): + if len(group_names) == 1: + groups.setdefault(leaf_name, groups[group_names[0]]) + + return groups + + +def _manifest_layer_index() -> dict[str, list[dict]]: + index = {} + for group_name, layers in _manifest_layer_groups().items(): + index.setdefault(group_name, []).extend(layers) + for layer in layers: + index.setdefault(_layer_key(layer["name"]), []).append(layer) + for alias in layer.get("aliases", []): + index.setdefault(_layer_key(alias), []).append(layer) + + all_layers = [] + for layers in index.values(): + all_layers.extend(layers) + index["all"] = list({id(layer): layer for layer in all_layers}.values()) + return index + + +def _download_s3_file(source: str, destination: Path): + parsed = urlparse(source) + if parsed.scheme != "s3" or not parsed.netloc or not parsed.path: + raise ValueError(f"Invalid S3 source: {source}") + + try: + import boto3 + except ImportError as exc: + raise RuntimeError("boto3 is required to download base layers from S3") from exc + + destination.parent.mkdir(parents=True, exist_ok=True) + temp_destination = destination.with_suffix(destination.suffix + ".part") + + logger.info("Downloading %s to %s", source, destination) + try: + client_kwargs = {} + try: + from django.conf import settings + + if settings.S3_ACCESS_KEY and settings.S3_SECRET_KEY: + client_kwargs.update( + aws_access_key_id=settings.S3_ACCESS_KEY, + aws_secret_access_key=settings.S3_SECRET_KEY, + ) + if getattr(settings, "S3_REGION", None): + client_kwargs["region_name"] = settings.S3_REGION + except Exception: + logger.debug( + "Django S3 settings unavailable; using boto3 credential provider chain.", + exc_info=True, + ) + + boto3.client("s3", **client_kwargs).download_file( + parsed.netloc, + parsed.path.lstrip("/"), + str(temp_destination), + ) + temp_destination.replace(destination) + except Exception: + if temp_destination.exists(): + temp_destination.unlink() + raise + + +def ensure_manifest_base_layers(*layers): + """ + Downloads base layers declared in config_new.yaml. + + Accepted selectors: + - concrete layer names: terrain, mws, lulc_v3, slope_percentage + - group names: static_layers, periodic_layers, tehsil_level + - all + """ + selected_layers = layers or ("static_layers", "periodic_layers") + index = _manifest_layer_index() + + for selected_layer in selected_layers: + layer_key = _layer_key(selected_layer) + layer_specs = index.get(layer_key) + if layer_specs is None: + available_layers = ", ".join(sorted(index)) + raise ValueError( + f"Unknown manifest base layer '{selected_layer}'. " + f"Available layers/groups: {available_layers}" + ) + + for layer in layer_specs: + source = layer.get("source") + if not source: + logger.warning( + "Base layer %s has no source in %s; create it manually at %s.", + layer["name"], + CONFIG_NEW_PATH, + _local_path(layer["local_path"]), + ) + continue + + if layer.get("type") != "file": + raise ValueError( + f"Unsupported base layer type for '{layer.get('name')}': " + f"{layer.get('type')}" + ) + + local_path = _local_path(layer["local_path"]) + if local_path.exists(): + logger.info( + "Base layer %s already exists at %s, skipping.", + layer["name"], + local_path, + ) + continue + + if source.startswith("s3://"): + _download_s3_file(source, local_path) + else: + raise ValueError( + f"Unsupported source for base layer '{layer['name']}': {source}" + ) + + +def ensure_soi_tehsil(): + """ + Downloads the SOI tehsil GeoJSON from GeoServer if not already present. + This is a lightweight bootstrap; the full admin-boundary archive includes + more data but takes much longer to acquire. + """ + if SOI_TEHSIL_PATH.exists(): + logger.info("SOI tehsil layer already exists at %s, skipping.", SOI_TEHSIL_PATH) + return + + SOI_TEHSIL_PATH.parent.mkdir(parents=True, exist_ok=True) + + from utilities.constants import GEOSERVER_BASE + + wfs_url = f"{GEOSERVER_BASE}pan_india_asset/ows" + logger.info("Downloading SOI tehsil layer from GeoServer...") + try: + response = requests.get( + wfs_url, params=_SOI_WFS_PARAMS, timeout=600, stream=True + ) + response.raise_for_status() + except requests.RequestException as e: + logger.error("Failed to download SOI tehsil layer: %s", e) + return + + with open(SOI_TEHSIL_PATH, "wb") as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + logger.info("SOI tehsil layer saved to %s", SOI_TEHSIL_PATH) + + +def ensure_admin_boundary_data(): + """ + Downloads and extracts the full admin-boundary archive (~8 GB) from Google Drive. + Skipped if the input directory is already populated. + Requires `gdown` and `7z` to be available on PATH. + """ + if _is_dir_populated(ADMIN_BOUNDARY_INPUT_DIR): + logger.info("Admin boundary data already exists, skipping.") + return + + ADMIN_BOUNDARY_INPUT_DIR.mkdir(parents=True, exist_ok=True) + ADMIN_BOUNDARY_OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + + archive_path = PROJECT_ROOT / "dataset.7z" + logger.info("Downloading admin boundary data (~8 GB) from Google Drive...") + try: + subprocess.run( + ["gdown", _GDRIVE_ADMIN_BOUNDARY_FILE_ID, "-O", str(archive_path)], + check=True, + ) + except (subprocess.CalledProcessError, FileNotFoundError) as e: + logger.error("Failed to download admin boundary archive: %s", e) + return + + logger.info("Extracting admin boundary data...") + try: + subprocess.run( + ["7z", "x", str(archive_path), f"-o{DATA_DIR / 'admin-boundary'}"], + check=True, + ) + except (subprocess.CalledProcessError, FileNotFoundError) as e: + logger.error("Failed to extract admin boundary archive: %s", e) + return + finally: + if archive_path.exists(): + archive_path.unlink() + + logger.info("Admin boundary data ready at %s", ADMIN_BOUNDARY_INPUT_DIR) + + +def ensure_lulc_rasters(): + """ + Downloads any missing LULC v3 yearly rasters from Google Drive. + Files already present on disk are skipped — no re-download. + Requires `gdown` on PATH. Each file is ~7-8 GB. + """ + LULC_DIR.mkdir(parents=True, exist_ok=True) + + missing = [ + (filename, file_id) + for filename, file_id in _LULC_GDRIVE_FILES + if not (LULC_DIR / filename).exists() + ] + + if not missing: + logger.info("All LULC rasters already present at %s, skipping.", LULC_DIR) + return + + logger.info( + "%d LULC raster(s) missing, downloading: %s", + len(missing), + [f for f, _ in missing], + ) + + for filename, file_id in missing: + dest = LULC_DIR / filename + logger.info("Downloading %s (~7-8 GB)...", filename) + try: + subprocess.run( + ["gdown", file_id, "-O", str(dest)], + check=True, + ) + logger.info("Saved %s", dest) + except (subprocess.CalledProcessError, FileNotFoundError) as e: + logger.error("Failed to download %s: %s", filename, e) + if dest.exists(): + dest.unlink() + raise + + +def ensure_microwatershed(): + """ + Downloads the pan-India microwatershed GeoJSON from Google Drive if not already present. + Requires `gdown` on PATH. + Fill in _GDRIVE_MICROWATERSHED_FILE_ID above once the Drive link is available. + """ + if MICROWATERSHED_PATH.exists(): + logger.info( + "Microwatershed file already exists at %s, skipping.", MICROWATERSHED_PATH + ) + return + + if not _GDRIVE_MICROWATERSHED_FILE_ID: + logger.warning( + "Microwatershed file not found at %s and no Google Drive file ID is configured. " + "Set _GDRIVE_MICROWATERSHED_FILE_ID in base_layer_setup.py or place the file manually.", + MICROWATERSHED_PATH, + ) + return + + MICROWATERSHED_PATH.parent.mkdir(parents=True, exist_ok=True) + logger.info( + "Downloading Microwatershed_v2_with_details.geojson from Google Drive..." + ) + try: + subprocess.run( + ["gdown", _GDRIVE_MICROWATERSHED_FILE_ID, "-O", str(MICROWATERSHED_PATH)], + check=True, + ) + logger.info("Saved microwatershed file to %s", MICROWATERSHED_PATH) + except (subprocess.CalledProcessError, FileNotFoundError) as e: + logger.error("Failed to download microwatershed file: %s", e) + if MICROWATERSHED_PATH.exists(): + MICROWATERSHED_PATH.unlink() + raise + + +def _active_tehsil_locations(): + from geoadmin.models import TehsilSOI + + return TehsilSOI.objects.filter( + active_status=True, + district__active_status=True, + district__state__active_status=True, + ).values_list( + "district__state__state_name", + "district__district_name", + "tehsil_name", + ).order_by( + "district__state__state_name", + "district__district_name", + "tehsil_name", + ) + + +def _tehsil_watershed_details(state, district, tehsil): + from utilities.gee_utils import valid_gee_text + + state_slug = valid_gee_text(state.strip().lower()) + district_slug = valid_gee_text(district.strip().lower()) + tehsil_slug = valid_gee_text(tehsil.strip().lower()) + destination = ( + TEHSIL_WATERSHEDS_DIR + / state_slug + / district_slug + / f"{tehsil_slug}.gpkg" + ) + layer_name = f"mws:mws_{district_slug}_{tehsil_slug}" + return destination, layer_name + + +def ensure_tehsil_watershed(state, district, tehsil, force=False): + import geopandas as gpd + + from utilities.constants import GEOSERVER_BASE + + destination, layer_name = _tehsil_watershed_details(state, district, tehsil) + if destination.exists() and not force: + return destination + + wfs_url = f"{GEOSERVER_BASE}mws/ows" + params = { + "service": "WFS", + "version": "1.0.0", + "request": "GetFeature", + "typeName": layer_name, + "outputFormat": "application/json", + "srsName": "EPSG:4326", + } + temp_destination = destination.with_suffix(".tmp.gpkg") + + try: + response = requests.get(wfs_url, params=params, timeout=600) + response.raise_for_status() + payload = response.json() + if ( + not isinstance(payload, dict) + or payload.get("type") != "FeatureCollection" + ): + raise ValueError("GeoServer did not return a FeatureCollection") + + watersheds = gpd.GeoDataFrame.from_features( + payload.get("features", []), + crs="EPSG:4326", + ) + if watersheds.empty: + raise ValueError("GeoServer layer is empty") + + destination.parent.mkdir(parents=True, exist_ok=True) + if temp_destination.exists(): + temp_destination.unlink() + watersheds.to_file( + temp_destination, + layer="watersheds", + driver="GPKG", + ) + temp_destination.replace(destination) + except Exception: + if temp_destination.exists(): + temp_destination.unlink() + raise + + logger.info("Saved %s to %s", layer_name, destination) + return destination + + +def _download_active_tehsil_watersheds(force=False): + locations = list(_active_tehsil_locations()) + if not locations: + logger.warning("No active tehsils found; no watershed files downloaded.") + return + + written = 0 + skipped = 0 + failures = [] + + for state, district, tehsil in locations: + destination, layer_name = _tehsil_watershed_details( + state, + district, + tehsil, + ) + if destination.exists() and not force: + skipped += 1 + continue + + try: + ensure_tehsil_watershed( + state=state, + district=district, + tehsil=tehsil, + force=force, + ) + written += 1 + except Exception as exc: + failures.append(f"{layer_name}: {exc}") + logger.error("Failed to download %s: %s", layer_name, exc) + + logger.info( + "GeoServer tehsil watersheds complete: %d written, %d skipped, %d failed.", + written, + skipped, + len(failures), + ) + if failures: + raise RuntimeError( + "Failed to download watershed layers for active tehsils: " + + "; ".join(failures) + ) + + +def with_tehsil_watershed(func): + func_signature = signature(func) + + @wraps(func) + def wrapper(*args, **kwargs): + call = func_signature.bind_partial(*args, **kwargs) + call.apply_defaults() + compute = str(call.arguments.get("compute", "local")).strip().lower() + if compute == "local": + ensure_tehsil_watershed( + state=call.arguments["state"], + district=call.arguments["district"], + tehsil=call.arguments.get("block") or call.arguments.get("tehsil"), + ) + return func(*args, **kwargs) + + return wrapper + + +def ensure_tehsil_watersheds(geoserver=False, force=False): + """ + Generates per-tehsil watershed .gpkg files by spatially intersecting the + microwatershed dataset against SOI tehsil boundaries. + Existing files are skipped unless force is true. When geoserver is true, + only active tehsils are downloaded from the mws workspace. + Both source files (SOI tehsil + microwatershed) must exist first. + """ + if geoserver: + _download_active_tehsil_watersheds(force=force) + return + + if _is_dir_populated(TEHSIL_WATERSHEDS_DIR) and not force: + logger.info( + "Tehsil watershed files already present at %s, skipping.", + TEHSIL_WATERSHEDS_DIR, + ) + return + + if not SOI_TEHSIL_PATH.exists(): + logger.warning( + "Cannot generate tehsil watersheds: SOI tehsil file missing at %s.", + SOI_TEHSIL_PATH, + ) + return + + if not MICROWATERSHED_PATH.exists(): + logger.warning( + "Cannot generate tehsil watersheds: microwatershed file missing at %s.", + MICROWATERSHED_PATH, + ) + return + + TEHSIL_WATERSHEDS_DIR.mkdir(parents=True, exist_ok=True) + logger.info("Generating tehsil watershed files (this may take a while)...") + + from computing.terrain_descriptor.store_watersheds_for_tehsils import ( + generate_tehsil_watershed_copies, + ) + + generate_tehsil_watershed_copies( + microwatershed_path=str(MICROWATERSHED_PATH), + tehsil_path=str(SOI_TEHSIL_PATH), + output_dir=str(TEHSIL_WATERSHEDS_DIR), + output_format="gpkg", + overwrite=force, + clip_to_tehsil=False, + ) + logger.info("Tehsil watershed files ready at %s", TEHSIL_WATERSHEDS_DIR) + + +def ensure_village_boundaries_dir(): + """ + Ensures the village boundaries directory exists. + TODO: add download logic once the source is determined. + """ + VILLAGE_BOUNDARIES_DIR.mkdir(parents=True, exist_ok=True) + + +_BASE_LAYER_ENSURERS = { + "soi_tehsil": ensure_soi_tehsil, + "admin_boundary": ensure_admin_boundary_data, + "lulc_rasters": ensure_lulc_rasters, + "microwatershed": ensure_microwatershed, + "tehsil_watersheds": ensure_tehsil_watersheds, + "village_boundaries": ensure_village_boundaries_dir, +} + +DEFAULT_BASE_LAYERS = ( + "static_layers", + "periodic_layers", +) + + +def setup_base_layers(*layers, geoserver=False, force=False): + selected_layers = layers or DEFAULT_BASE_LAYERS + manifest_index = _manifest_layer_index() + + for layer in selected_layers: + if layer in _BASE_LAYER_ENSURERS: + if layer == "tehsil_watersheds": + ensure_tehsil_watersheds(geoserver=geoserver, force=force) + else: + _BASE_LAYER_ENSURERS[layer]() + continue + + if _layer_key(layer) in manifest_index: + ensure_manifest_base_layers(layer) + continue + + legacy_layers = ", ".join(sorted(_BASE_LAYER_ENSURERS)) + manifest_layers = ", ".join(sorted(manifest_index)) + raise ValueError( + f"Unknown base layer '{layer}'. Legacy layers: {legacy_layers}. " + f"Manifest layers/groups: {manifest_layers}" + ) + + +def with_base_layers(*layers): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + setup_base_layers(*layers) + return func(*args, **kwargs) + + return wrapper + + return decorator + + +download_base_layers = with_base_layers diff --git a/computing/bulk_layer_generation.py b/computing/bulk_layer_generation.py new file mode 100644 index 00000000..75770fc1 --- /dev/null +++ b/computing/bulk_layer_generation.py @@ -0,0 +1,495 @@ +import inspect +from dataclasses import asdict, dataclass +from functools import reduce +from operator import or_ +from typing import Any, Callable, Mapping + +import requests +from django.conf import settings +from django.db.models import Q +from django.utils.module_loading import import_string + +from computing.models import Layer +from geoadmin.models import TehsilSOI +from utilities.pipelines import api_request_payload + + +@dataclass(frozen=True) +class Location: + state: str + district: str + block: str + + def asdict(self) -> dict[str, str]: + return asdict(self) + + +@dataclass(frozen=True) +class PipelineSpec: + runner_path: str + payload_builder: Callable[[Mapping[str, str], bool], dict[str, Any]] + dataset_names: tuple[str, ...] + + def run(self, location: Mapping[str, str], overwrite: bool) -> Any: + runner = import_string(self.runner_path) + return runner(self.payload_builder(location, overwrite)) + + +def _standard_payload( + location: Mapping[str, str], overwrite: bool +) -> dict[str, Any]: + return api_request_payload( + { + "state": location["state"], + "district": location["district"], + "block": location["block"], + "overwrite": overwrite, + }, + overwrite=overwrite, + ) + + +STANDARD_PIPELINES = { + "antyodaya": PipelineSpec( + "computing.misc.antyodaya.run_antyodaya_request", + _standard_payload, + ("Antyodaya 2020",), + ), + "facilities_proximity": PipelineSpec( + "computing.misc.facilities.run_facilities_request", + _standard_payload, + ("Facilities Points", "Facilities Proximity"), + ), + "livestocks": PipelineSpec( + "computing.misc.livestocks.run_livestocks_request", + _standard_payload, + ("Livestock Census 2019",), + ), +} + +LOCAL_TASK_DATASETS = { + "computing.misc.nrega_local_compute.generate_nrega_data_local": ("NREGA Assets",), + "computing.lulc.lulc_v3_local.clip_lulc_v3": ("LULC_v3", "LULC_level_3"), + "computing.lulc.lulc_vector_local.vectorise_lulc": ("LULC",), + "computing.cropping_intensity.cropping_intesity_local.generate_cropping_intensity": ( + "Cropping Intensity", + ), + "computing.surface_water_bodies.swb_local.generate_swb_layer": ( + "Surface Water Bodies", + ), + "computing.change_detection.change_detection_local.get_change_detection": ( + "Change Detection Raster", + ), + "computing.change_detection.change_detection_vector_local.vectorise_change_detection": ( + "Change Detection Vector", + ), + "computing.misc.aquifer_vector_local.generate_aquifer_vector": ("Aquifer",), + "computing.terrain_descriptor.terrain_raster_fabdem_local.generate_terrain_raster_clip": ( + "Terrain Raster", + ), + "computing.terrain_descriptor.terrain_clusters_local.generate_terrain_clusters": ( + "Terrain Vector", + ), + "computing.terrain_descriptor.terrain_compute_all_local.generate_terrain_compute_all": ( + "Terrain Raster", + "Terrain Vector", + ), + "computing.lulc_X_terrain.lulc_on_plain_cluster_local.lulc_on_plain_cluster_local": ( + "Terrain LULC", + ), + "computing.lulc_X_terrain.lulc_on_slope_cluster_local.lulc_on_slope_cluster_local": ( + "Terrain LULC", + ), + "computing.misc.soge_vector_local_compute.generate_soge_vector_local": ("SOGE",), + "computing.misc.drainage_lines_local_compute.clip_drainage_lines": ("Drainage",), + "computing.misc.naturaldepression_local_compute.generate_natural_depression_data_local": ( + "Natural Depression", + ), + "computing.misc.distancetonearestdrainage_local_compute.generate_distance_to_nearest_drainage_line_local": ( + "Distance to Drainage Line", + ), + "computing.misc.catchment_area_local_compute.generate_catchment_area_singleflow_local": ( + "Catchment Area", + ), + "computing.misc.slope_percentage_local_compute.generate_slope_percentage_data_local": ( + "Slope Percentage", + ), + "computing.misc.lcw_conflict_local_compute.generate_lcw_conflict_data_local": ( + "LCW Conflict", + ), + "computing.misc.agroecological_space_local_compute.generate_agroecological_data_local": ( + "Agroecological", + ), + "computing.misc.factory_csr_local_compute.generate_factory_csr_data_local": ( + "Factory CSR", + ), + "computing.misc.green_credit_local_compute.generate_green_credit_data_local": ( + "Green Credit", + ), + "computing.misc.mining_data_local_compute.generate_mining_data_local": ("Mining",), + "computing.misc.restoration_opportunity_local_compute.generate_restoration_opportunity_local": ( + "Restoration Raster", + "Restoration Vector", + ), + "computing.mws.mws_connectivity_local_compute.mws_connectivity_vector": ( + "Mws Connectivity", + ), + "computing.misc.facilities_proximity_local_compute.generate_facilities_proximity_local": ( + "Facilities Proximity", + ), + "computing.misc.livestocks_local_compute.generate_livestocks_data_local": ( + "Livestock Census 2019", + ), + "computing.misc.antyodaya_local_compute.generate_antyodaya_data_local": ( + "Antyodaya 2020", + ), + "computing.misc.drainage_density_local_compute.drainage_density": ( + "Drainage Density Vector", + ), + "computing.misc.river_local_compute.river_vector": ("River Vector",), + "computing.misc.canal_local_compute.canal_vector": ("Canal Vector",), + "computing.misc.digital_elevation_model_local.generate_febdem_raster_vector_clip": ( + "DEM Raster", + "DEM Vector", + ), + "computing.mws.mws_centroid_local_compute.generate_mws_centroid_data_local": ( + "Mws Centroid", + ), + "computing.tree_health.local.canopy_height_local.tree_health_ch_raster_local": ( + "Canopy Height Raster", + ), + "computing.tree_health.local.canopy_height_vector_local.tree_health_ch_vector_local": ( + "Canopy Height Vector", + ), + "computing.tree_health.local.ccd_local.tree_health_ccd_raster_local": ( + "Ccd Raster", + ), + "computing.tree_health.local.ccd_vector_local.tree_health_ccd_vector_local": ( + "Ccd Vector", + ), + "computing.tree_health.local.overall_change_local.tree_health_overall_change_raster_local": ( + "Tree Overall Change Raster", + ), + "computing.tree_health.local.overall_change_vector_local.tree_health_overall_change_vector_local": ( + "Tree Overall Change Vector", + ), + "computing.soil_health.soil_health.soil_health_local": ( + "Soil Health Raster", + "Soil Health Vector", + ), + "computing.soil_type.soil_type_local.generate_soil_type_local": ("Soil Type",), +} + + +def _task_registry(compute: str): + from computing.layer_dependency.layer_generation_in_order import TASK_REGISTRIES + + return TASK_REGISTRIES[compute] + + +def _normalize_compute(compute: str) -> str: + value = str(compute or "local").strip().lower() + if value not in {"gee", "local"}: + raise ValueError("compute must be either 'gee' or 'local'") + return value + + +def pipeline_names(compute: str = "local") -> tuple[str, ...]: + compute = _normalize_compute(compute) + names = set(_task_registry(compute)) + if compute == "local": + names.update(STANDARD_PIPELINES) + return tuple(sorted(names)) + + +def get_pipeline(name: str, compute: str = "local"): + compute = _normalize_compute(compute) + if compute == "local" and name in STANDARD_PIPELINES: + return STANDARD_PIPELINES[name] + + try: + return _task_registry(compute)[name] + except KeyError as exc: + raise ValueError( + f"Unknown {compute} pipeline '{name}'. Use --list-pipelines " + "to see available pipelines." + ) from exc + + +def get_regeneration_dataset_names(name: str) -> tuple[str, ...]: + pipeline = get_pipeline(name, compute="local") + if isinstance(pipeline, PipelineSpec): + return pipeline.dataset_names + + try: + return LOCAL_TASK_DATASETS[pipeline.name] + except KeyError as exc: + raise ValueError( + f"Local regeneration datasets are not configured for pipeline '{name}'." + ) from exc + + +def _legacy_runner_kwargs( + runner, + location: Mapping[str, str], + *, + start_year: int | None, + end_year: int | None, + gee_account_id: str | None, + overwrite: bool, +) -> dict[str, Any]: + target = getattr(runner, "run", runner) + parameters = inspect.signature(target).parameters + accepts_kwargs = any( + parameter.kind == inspect.Parameter.VAR_KEYWORD + for parameter in parameters.values() + ) + available = { + "state": location["state"], + "district": location["district"], + "block": location["block"], + "start_year": start_year, + "end_year": end_year, + "gee_account_id": gee_account_id, + } + kwargs = { + name: value + for name, value in available.items() + if value is not None and (accepts_kwargs or name in parameters) + } + if accepts_kwargs or "overwrite" in parameters: + kwargs["overwrite"] = overwrite + if "is_override" in parameters: + kwargs["is_override"] = overwrite + + missing = [ + name + for name, parameter in parameters.items() + if name != "self" + and parameter.default is inspect.Parameter.empty + and parameter.kind + in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY) + and name not in kwargs + ] + if missing: + raise ValueError( + f"Pipeline requires unsupported or missing arguments: {', '.join(missing)}" + ) + return kwargs + + +def validate_pipeline( + name: str, + *, + compute: str = "local", + start_year: int | None = None, + end_year: int | None = None, + gee_account_id: str | None = None, + overwrite: bool = True, +) -> None: + runner = get_pipeline(name, compute) + if isinstance(runner, PipelineSpec): + return + _legacy_runner_kwargs( + runner, + {"state": "state", "district": "district", "block": "block"}, + start_year=start_year, + end_year=end_year, + gee_account_id=gee_account_id, + overwrite=overwrite, + ) + + +def run_pipeline( + name: str, + location: Mapping[str, str], + overwrite: bool = True, + *, + compute: str = "local", + start_year: int | None = None, + end_year: int | None = None, + gee_account_id: str | None = None, +) -> Any: + runner = get_pipeline(name, compute) + if isinstance(runner, PipelineSpec): + return runner.run(location, overwrite) + kwargs = _legacy_runner_kwargs( + runner, + location, + start_year=start_year, + end_year=end_year, + gee_account_id=gee_account_id, + overwrite=overwrite, + ) + return runner(**kwargs) + + +def get_active_locations( + *, + state: str | None = None, + district: str | None = None, + block: str | None = None, + blocks: list[str] | None = None, + limit: int | None = None, +) -> list[Location]: + if block and blocks: + raise ValueError("Use either block or blocks, not both.") + + queryset = ( + TehsilSOI.objects.filter( + active_status=True, + district__active_status=True, + district__state__active_status=True, + ) + .select_related("district__state") + .order_by( + "district__state__state_name", + "district__district_name", + "tehsil_name", + "pk", + ) + ) + if state: + queryset = queryset.filter(district__state__state_name__iexact=state) + if district: + queryset = queryset.filter(district__district_name__iexact=district) + selected_blocks = [block] if block else list(dict.fromkeys(blocks or [])) + if selected_blocks: + queryset = queryset.filter( + reduce( + or_, + (Q(tehsil_name__iexact=name) for name in selected_blocks), + ) + ) + if limit is not None: + queryset = queryset[:limit] + + return [ + Location( + state=tehsil.district.state.state_name, + district=tehsil.district.district_name, + block=tehsil.tehsil_name, + ) + for tehsil in queryset + ] + + +def get_locally_generated_locations( + *, + dataset_names: tuple[str, ...], + state: str | None = None, + district: str | None = None, + block: str | None = None, + blocks: list[str] | None = None, + limit: int | None = None, +) -> list[Location]: + if block and blocks: + raise ValueError("Use either block or blocks, not both.") + + queryset = Layer.objects.filter( + dataset__name__in=dataset_names, + misc__is_generated_locally=True, + ) + if state: + queryset = queryset.filter(state__state_name__iexact=state) + if district: + queryset = queryset.filter(district__district_name__iexact=district) + selected_blocks = [block] if block else list(dict.fromkeys(blocks or [])) + if selected_blocks: + queryset = queryset.filter( + reduce( + or_, + (Q(block__tehsil_name__iexact=name) for name in selected_blocks), + ) + ) + + locations = ( + queryset.values( + "state__state_name", + "district__district_name", + "block__tehsil_name", + ) + .order_by( + "state__state_name", + "district__district_name", + "block__tehsil_name", + ) + .distinct() + ) + if limit is not None: + locations = locations[:limit] + + return [ + Location( + state=location["state__state_name"], + district=location["district__district_name"], + block=location["block__tehsil_name"], + ) + for location in locations + ] + + +def get_active_locations_from_api( + *, + state: str | None = None, + district: str | None = None, + block: str | None = None, + blocks: list[str] | None = None, + limit: int | None = None, +) -> list[Location]: + if block and blocks: + raise ValueError("Use either block or blocks, not both.") + + backend_url = getattr(settings, "PROD_BACKEND_URL", "").rstrip("/") + if not backend_url: + raise ValueError("PROD_BACKEND_URL is not configured.") + + endpoint = f"{backend_url}/api/v1/proposed_blocks/" + try: + response = requests.get(endpoint, timeout=30) + response.raise_for_status() + payload = response.json() + except requests.RequestException as exc: + raise ValueError( + f"Failed to fetch active locations from {endpoint}: {exc}" + ) from exc + if not isinstance(payload, list): + raise ValueError("Active locations API returned an invalid response.") + + state_filter = state.casefold() if state else None + district_filter = district.casefold() if district else None + selected_blocks = { + name.casefold() for name in ([block] if block else blocks or []) + } + locations = [] + + try: + for state_data in payload: + state_name = state_data["label"] + if state_filter and state_name.casefold() != state_filter: + continue + for district_data in state_data["district"]: + district_name = district_data["label"] + if ( + district_filter + and district_name.casefold() != district_filter + ): + continue + for block_data in district_data["blocks"]: + block_name = block_data["label"] + if ( + selected_blocks + and block_name.casefold() not in selected_blocks + ): + continue + locations.append( + Location(state_name, district_name, block_name) + ) + if limit is not None and len(locations) >= limit: + return locations + except (KeyError, TypeError, AttributeError) as exc: + raise ValueError( + "Active locations API returned an invalid response." + ) from exc + + return locations diff --git a/computing/celery_task_logging.py b/computing/celery_task_logging.py new file mode 100644 index 00000000..cfb7db99 --- /dev/null +++ b/computing/celery_task_logging.py @@ -0,0 +1,69 @@ +""" +Celery signal handlers for layer-generation task observability. +""" + +import logging + +from celery.signals import task_failure, task_postrun, task_prerun + +logger = logging.getLogger("core_stack.layer_generation") + + +@task_prerun.connect +def log_layer_task_prerun( + sender=None, task_id=None, task=None, args=None, kwargs=None, **extra +): + logger.info( + "Celery task start | name=%s id=%s args=%s kwargs=%s", + getattr(sender, "name", sender), + task_id, + args, + kwargs, + ) + + +@task_postrun.connect +def log_layer_task_postrun( + sender=None, + task_id=None, + task=None, + args=None, + kwargs=None, + retval=None, + state=None, + **extra, +): + logger.info( + "Celery task finished | name=%s id=%s state=%s retval=%s", + getattr(sender, "name", sender), + task_id, + state, + retval, + ) + + +@task_failure.connect +def log_layer_task_failure( + sender=None, + task_id=None, + exception=None, + args=None, + kwargs=None, + traceback=None, + einfo=None, + **extra, +): + tb = None + if einfo is not None: + tb = getattr(einfo, "traceback", None) + if not tb: + tb = traceback + logger.error( + "Celery task failed | name=%s id=%s args=%s kwargs=%s exception=%s\n%s", + getattr(sender, "name", sender), + task_id, + args, + kwargs, + exception, + tb or "", + ) diff --git a/computing/change_detection/change_detection.py b/computing/change_detection/change_detection.py index f1a13b55..3803b0b7 100644 --- a/computing/change_detection/change_detection.py +++ b/computing/change_detection/change_detection.py @@ -1,5 +1,4 @@ import ee -import copy from utilities.gee_utils import ( ee_initialize, check_task_status, @@ -17,7 +16,7 @@ @app.task(bind=True) def get_change_detection( - self, state, district, block, start_year, end_year, gee_account_id + self, state, district, block, start_year, end_year, gee_account_id ): """ This function will generate change detection raster for urbanization, Degradation, @@ -31,6 +30,7 @@ def get_change_detection( "Deforestation": change_deforestation, "Afforestation": change_afforestation, "CropIntensity": change_cropping_intensity, + "ShrubChange": change_shrub, } description = ( f"change_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}" @@ -116,6 +116,20 @@ def get_change_detection( return layer_at_geoserver +def _compute_then_now_modes(l1_asset_remapped, lulc_projection, roi_boundary): + if len(l1_asset_remapped) < 6: + raise ValueError( + "Change detection requires at least six yearly LULC rasters to compare the first three years against the last three years." + ) + + then = ee.ImageCollection(l1_asset_remapped[:3]).mode().reproject(lulc_projection) + now = ee.ImageCollection(l1_asset_remapped[-3:]).mode().reproject(lulc_projection) + + then = then.clip(roi_boundary.geometry()) + now = now.clip(roi_boundary.geometry()) + return now, then + + def built_up(roi_boundary, l1_asset): print("built_up function is runing") @@ -132,13 +146,9 @@ def remap_values(image): l1_asset_remapped = [remap_values(asset) for asset in l1_asset] - # Create image collections - then = ee.ImageCollection(l1_asset_remapped[:3]).mode().reproject(lulc_projection) - now = ee.ImageCollection(l1_asset_remapped[-3:]).mode().reproject(lulc_projection) - - # Compute mode and clip - then = then.clip(roi_boundary.geometry()) - now = now.clip(roi_boundary.geometry()) + now, then = _compute_then_now_modes( + l1_asset_remapped, lulc_projection, roi_boundary + ) # Compute transitions trans_bu_bu = then.eq(1).And(now.eq(1)) @@ -172,13 +182,9 @@ def remap_values(image): l1_asset_remapped = [remap_values(asset) for asset in l1_asset] - # Create image collections - then = ee.ImageCollection(l1_asset_remapped[:3]).mode().reproject(lulc_projection) - now = ee.ImageCollection(l1_asset_remapped[-3:]).mode().reproject(lulc_projection) - - # Compute mode and clip - then = then.clip(roi_boundary.geometry()) - now = now.clip(roi_boundary.geometry()) + now, then = _compute_then_now_modes( + l1_asset_remapped, lulc_projection, roi_boundary + ) trans_f_f = then.eq(3).And(now.eq(3)) trans_f_bu = then.eq(3).And(now.eq(1)).multiply(2) @@ -197,160 +203,23 @@ def remap_values(image): return change_deg -def change_deforestation_afforestation(roi_boundary, l1_asset, lulc_projection): - print("change_deforestation is running") - # Create an initial zero image - zero_image2 = ( - ee.Image.constant(0) - .setDefaultProjection(lulc_projection) - .clip(l1_asset[0].geometry()) - ) - - # for i in range(1, 5): - for i in range(1, len(l1_asset) - 1): - before = l1_asset[i - 1] - middle = l1_asset[i] - after = l1_asset[i + 1] - - cond1 = ( - before.eq(12) - .And(after.eq(12)) - .And( - middle.eq(6) - .Or(middle.eq(8)) - .Or(middle.eq(9)) - .Or(middle.eq(10)) - .Or(middle.eq(11)) - ) - ) - cond2 = ( - before.eq(2) - .Or(before.eq(3)) - .Or(before.eq(4)) - .And(after.eq(2).Or(after.eq(3)).Or(after.eq(4))) - .And( - middle.eq(6) - .Or(middle.eq(8)) - .Or(middle.eq(9)) - .Or(middle.eq(10)) - .Or(middle.eq(11)) - ) - ) - cond3 = before.eq(6).And(after.eq(6)).And(middle.eq(12)) - cond4 = ( - before.eq(8) - .Or(before.eq(9)) - .Or(before.eq(10)) - .Or(before.eq(11)) - .And(after.eq(8).Or(after.eq(9)).Or(after.eq(10)).Or(after.eq(11))) - .And(middle.eq(12)) - ) - cond5 = ( - before.eq(8) - .Or(before.eq(9)) - .Or(before.eq(10)) - .Or(before.eq(11)) - .And(after.eq(8).Or(after.eq(9)).Or(after.eq(10)).Or(after.eq(11))) - .And(middle.eq(7)) - ) - cond6 = ( - before.eq(6) - .And(after.eq(6)) - .And(middle.eq(8).Or(middle.eq(9)).Or(middle.eq(10)).Or(middle.eq(11))) - ) - cond7 = ( - before.eq(8) - .Or(before.eq(9)) - .Or(before.eq(10)) - .Or(before.eq(11)) - .And(after.eq(8).Or(after.eq(9)).Or(after.eq(10)).Or(after.eq(11))) - .And(middle.eq(6)) - ) - cond8 = before.eq(1).And(after.eq(1)).And(middle.eq(6)) - cond9 = before.eq(6).And(after.eq(6)).And(middle.eq(1)) - cond10 = ( - before.eq(1) - .And(after.eq(1)) - .And(middle.eq(8).Or(middle.eq(9)).Or(middle.eq(10)).Or(middle.eq(11))) - ) - cond11 = ( - before.eq(7) - .And(after.eq(7)) - .And( - middle.eq(6) - .Or(middle.eq(8)) - .Or(middle.eq(9)) - .Or(middle.eq(10)) - .Or(middle.eq(11)) - ) - ) - - zero_image2 = ( - zero_image2.add(cond1) - .add(cond2) - .add(cond3) - .add(cond4) - .add(cond5) - .add(cond6) - .add(cond7) - .add(cond8) - .add(cond9) - .add(cond10) - .add(cond11) - ) - - l1_asset_copy = copy.deepcopy(l1_asset) - for i in range(1, len(l1_asset) - 1): - # for i in range(1, 5): - before = l1_asset[i - 1] - middle = l1_asset[i] - after = l1_asset[i + 1] - - cond1 = ( - before.eq(3) - .And(middle.neq(3)) - .And(after.eq(3)) - .And((zero_image2.eq(3).Or(zero_image2.eq(4)))) - ) - cond2 = ( - before.neq(3) - .And(middle.eq(3)) - .And(after.neq(3)) - .And((zero_image2.eq(3).Or(zero_image2.eq(4)))) - ) - - middle = middle.where(cond1, 3) - middle = middle.where(cond2, before) - - l1_asset_copy[i] = middle +def change_deforestation(roi_boundary, l1_asset): + lulc_projection = l1_asset[0].projection() - # Remap values function def remap_values(image): - remapped = image.remap( + return image.remap( [1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12], [1, 2, 2, 2, 3, 5, 4, 4, 4, 4, 6], 0, "predicted_label", ).setDefaultProjection(lulc_projection) - return remapped - - l1_asset_remapped = [remap_values(asset) for asset in l1_asset_copy] - - # Create image collections - then = ee.ImageCollection(l1_asset_remapped[:3]).mode().reproject(lulc_projection) - now = ee.ImageCollection(l1_asset_remapped[-3:]).mode().reproject(lulc_projection) - - # Compute mode and clip - then = then.clip(roi_boundary.geometry()) - now = now.clip(roi_boundary.geometry()) - return now, then + l1_asset_remapped = [remap_values(asset) for asset in l1_asset] -def change_deforestation(roi_boundary, l1_asset): - lulc_projection = l1_asset[0].projection() - now, then = change_deforestation_afforestation( - roi_boundary, l1_asset, lulc_projection + now, then = _compute_then_now_modes( + l1_asset_remapped, lulc_projection, roi_boundary ) + trans_fo_fo = then.eq(3).And(now.eq(3)) trans_fo_bu = then.eq(3).And(now.eq(1)).multiply(2) trans_fo_fa = then.eq(3).And(now.eq(4)).multiply(3) @@ -374,9 +243,21 @@ def change_deforestation(roi_boundary, l1_asset): def change_afforestation(roi_boundary, l1_asset): lulc_projection = l1_asset[0].projection() - now, then = change_deforestation_afforestation( - roi_boundary, l1_asset, lulc_projection + + def remap_values(image): + return image.remap( + [1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12], + [1, 2, 2, 2, 3, 5, 4, 4, 4, 4, 6], + 0, + "predicted_label", + ).setDefaultProjection(lulc_projection) + + l1_asset_remapped = [remap_values(asset) for asset in l1_asset] + + now, then = _compute_then_now_modes( + l1_asset_remapped, lulc_projection, roi_boundary ) + trans_fo_fo = then.eq(3).And(now.eq(3)) trans_bu_fo = then.eq(1).And(now.eq(3)).multiply(2) trans_fa_fo = then.eq(4).And(now.eq(3)).multiply(3) @@ -413,13 +294,9 @@ def remap_values(image): l1_asset_remapped = [remap_values(asset) for asset in l1_asset] - # Create image collections - then = ee.ImageCollection(l1_asset_remapped[:3]).mode().reproject(lulc_projection) - now = ee.ImageCollection(l1_asset_remapped[-3:]).mode().reproject(lulc_projection) - - # Compute mode and clip - then = then.clip(roi_boundary.geometry()) - now = now.clip(roi_boundary.geometry()) + now, then = _compute_then_now_modes( + l1_asset_remapped, lulc_projection, roi_boundary + ) trans_do_si = then.eq(6).And(now.eq(5)) trans_tr_si = then.eq(7).And(now.eq(5)).multiply(2) @@ -457,8 +334,51 @@ def remap_values(image): return change_far +def change_shrub(roi_boundary, l1_asset): + lulc_projection = l1_asset[0].projection() + # shrub -> shrub, shrub -> crops, shrub -> tree, shrub -> built-up, shrub -> water + + # Remap values function + def remap_values(image): + return image.remap( + [1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12], + [1, 2, 2, 2, 3, 4, 5, 5, 5, 5, 6], + 0, + "predicted_label", + ).setDefaultProjection(lulc_projection) + + l1_asset_remapped = [remap_values(asset) for asset in l1_asset] + + now, then = _compute_then_now_modes( + l1_asset_remapped, lulc_projection, roi_boundary + ) + + trans_sh_sh = then.eq(6).And(now.eq(6)) + trans_sh_fa = then.eq(6).And(now.eq(5)).multiply(2) + trans_sh_tr = then.eq(6).And(now.eq(3)).multiply(3) + trans_sh_bu = then.eq(6).And(now.eq(1)).multiply(4) + trans_sh_wa = then.eq(6).And(now.eq(2)).multiply(5) + trans_sh_ba = then.eq(6).And(now.eq(4)).multiply(6) + + # Create a zero image and add transitions + change_shr = ( + ee.Image.constant(0) + .setDefaultProjection(lulc_projection) + .clip(roi_boundary.geometry()) + ) + change_shr = ( + change_shr.add(trans_sh_sh) + .add(trans_sh_fa) + .add(trans_sh_tr) + .add(trans_sh_bu) + .add(trans_sh_wa) + .add(trans_sh_ba) + ) + return change_shr + + def sync_to_gcs_geoserver( - state, district, block, description, param_list, layer_ids, start_year, end_year + state, district, block, description, param_list, layer_ids, start_year, end_year ): task_list = [] diff --git a/computing/change_detection/change_detection_local.py b/computing/change_detection/change_detection_local.py new file mode 100644 index 00000000..85728ed5 --- /dev/null +++ b/computing/change_detection/change_detection_local.py @@ -0,0 +1,652 @@ +import os +import uuid +from concurrent.futures import ThreadPoolExecutor + +import numpy as np +import rasterio +from rasterio.mask import mask +from rasterio.warp import Resampling, reproject +from shapely.geometry import mapping +from utilities.gee_utils import valid_gee_text + +from nrm_app.celery import app + +from computing.config_loader import ( + CHANGE_DETECTION_RASTER_OUTPUT_DIR as LOCAL_OUTPUT_BASE_DIR, +) +from computing.local_compute_helper import ( + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + build_output_raster_path, + get_union_geometry, + load_precomputed_roi, + push_local_raster_to_geoserver, + resolve_lulc_raster_paths, + validate_geometry, +) +from computing.utils import save_layer_info_to_db, update_layer_sync_status + +GEOSERVER_WORKSPACE = "change_detection" + +CHANGE_STAC_LAYER_NAMES = { + "Urbanization": "change_urbanization_raster", + "Degradation": "change_cropping_reduction_raster", + "Deforestation": "change_tree_cover_loss_raster", + "Afforestation": "change_tree_cover_gain_raster", + "CropIntensity": "change_cropping_intensity_raster", + "ShrubChange": "change_shrub_change_raster", +} + +BUILT_UP_REMAP = { + 1: 1, + 2: 2, + 3: 2, + 4: 2, + 6: 3, + 7: 4, + 8: 3, + 9: 3, + 10: 3, + 11: 3, + 12: 4, +} + +DEGRADATION_REMAP = { + 1: 1, + 2: 2, + 3: 2, + 4: 2, + 6: 4, + 7: 5, + 8: 3, + 9: 3, + 10: 3, + 11: 3, + 12: 6, +} + +DEFORESTATION_AFFORESTATION_REMAP = { + 1: 1, + 2: 2, + 3: 2, + 4: 2, + 6: 3, + 7: 5, + 8: 4, + 9: 4, + 10: 4, + 11: 4, + 12: 6, +} + +CROP_INTENSITY_REMAP = { + 1: 1, + 2: 2, + 3: 2, + 4: 2, + 6: 3, + 7: 4, + 8: 5, + 9: 5, + 10: 6, + 11: 7, + 12: 8, +} + +SHRUB_REMAP = { + 1: 1, + 2: 2, + 3: 2, + 4: 2, + 6: 3, + 7: 4, + 8: 5, + 9: 5, + 10: 5, + 11: 5, + 12: 6, +} + + +def _build_lookup_table(mapping, size=13): + lookup = np.zeros(size, dtype=np.int16) + for source_value, mapped_value in mapping.items(): + lookup[source_value] = mapped_value + return lookup + + +BUILT_UP_LOOKUP = _build_lookup_table(BUILT_UP_REMAP) +DEGRADATION_LOOKUP = _build_lookup_table(DEGRADATION_REMAP) +DEFORESTATION_AFFORESTATION_LOOKUP = _build_lookup_table( + DEFORESTATION_AFFORESTATION_REMAP +) +CROP_INTENSITY_LOOKUP = _build_lookup_table(CROP_INTENSITY_REMAP) +SHRUB_LOOKUP = _build_lookup_table(SHRUB_REMAP) + +CHANGE_PARAM_FUNCTIONS = { + "Urbanization": "_compute_built_up_change", + "Degradation": "_compute_degradation_change", + "Deforestation": "_compute_deforestation_change", + "Afforestation": "_compute_afforestation_change", + "CropIntensity": "_compute_crop_intensity_change", + "ShrubChange": "_compute_shrub_change", +} + +ZERO_NODATA = 0 + + +def _slug(value, fallback): + return valid_gee_text(str(value).strip().lower()) or fallback + + +def _as_lulc_int_array(array): + array = np.asarray(array) + if np.issubdtype(array.dtype, np.integer): + return array.astype(np.int16, copy=False) + + array = np.asarray(array, dtype=np.float64) + array = np.where(np.isfinite(array), array, 0) + return np.rint(array).astype(np.int16, copy=False) + + +def _remap_array(array, mapping): + source = _as_lulc_int_array(array) + if isinstance(mapping, np.ndarray): + return mapping[source] + + remapped = np.zeros(source.shape, dtype=np.int16) + for source_value, mapped_value in mapping.items(): + remapped[source == source_value] = mapped_value + return remapped + + +def _combine_transitions(shape, transitions): + result = np.zeros(shape, dtype=np.uint8) + for value, condition in transitions: + result += condition.astype(np.uint8) * np.uint8(value) + return result + + +def _base_description(district, block): + return ( + f"change_{_slug(district, 'unknown_district')}_{_slug(block, 'unknown_block')}" + ) + + +def _published_layer_name(district, block, param_name): + return f"{_base_description(district, block)}_{param_name}" + + +def _output_stub(district, block, param_name, start_year, end_year): + return ( + f"{_published_layer_name(district, block, param_name)}_{start_year}_{end_year}" + ) + + +def _select_change_detection_raster_paths(start_year, end_year): + raster_paths = resolve_lulc_raster_paths(start_year=start_year, end_year=end_year) + if len(raster_paths) < 6: + raise ValueError( + "Local change detection requires at least six yearly LULC rasters to compare the first three years against the last three years." + ) + return raster_paths[:3] + raster_paths[-3:] + + +def _build_roi_shapes_by_crs(roi_gdf, raster_paths): + roi_shapes_by_crs = {} + roi_union = get_union_geometry(roi_gdf) + if roi_union is None or roi_union.is_empty: + raise ValueError("ROI union geometry is empty for local change detection.") + + roi_shapes_by_crs[str(roi_gdf.crs)] = mapping(roi_union) + for raster_path in raster_paths: + with rasterio.open(raster_path) as src: + crs_key = str(src.crs) + if crs_key in roi_shapes_by_crs: + continue + clip_gdf = roi_gdf if src.crs is None else roi_gdf.to_crs(src.crs) + clip_union = get_union_geometry(clip_gdf) + if clip_union is None or clip_union.is_empty: + raise ValueError( + "ROI union geometry is empty for local change detection." + ) + roi_shapes_by_crs[crs_key] = mapping(clip_union) + + return roi_shapes_by_crs + + +def _load_masked_lulc_array(index, raster_path, roi_shape_by_crs): + with rasterio.open(raster_path) as src: + clipped_data, clipped_transform = mask( + src, + shapes=[roi_shape_by_crs[str(src.crs)]], + crop=True, + filled=True, + nodata=ZERO_NODATA, + ) + clipped_array = _as_lulc_int_array(clipped_data[0]) + meta = src.meta.copy() + return { + "index": index, + "raster_path": raster_path, + "array": clipped_array, + "transform": clipped_transform, + "crs": src.crs, + "meta": meta, + } + + +def _load_masked_lulc_arrays(roi_gdf, raster_paths): + roi_gdf = validate_geometry(roi_gdf) + if roi_gdf.empty: + raise ValueError("No valid ROI geometry available for local change detection.") + if roi_gdf.crs is None: + raise ValueError("ROI CRS is missing; cannot align LULC rasters.") + + roi_shapes_by_crs = _build_roi_shapes_by_crs(roi_gdf, raster_paths) + max_workers = min(len(raster_paths), max(os.cpu_count() or 1, 1)) + with ThreadPoolExecutor(max_workers=max_workers) as executor: + futures = [ + executor.submit( + _load_masked_lulc_array, index, raster_path, roi_shapes_by_crs + ) + for index, raster_path in enumerate(raster_paths, start=1) + ] + results = [future.result() for future in futures] + + results.sort(key=lambda item: item["index"]) + + reference = results[0] + output_meta = reference["meta"] + output_meta.update( + { + "driver": "GTiff", + "height": reference["array"].shape[0], + "width": reference["array"].shape[1], + "transform": reference["transform"], + "crs": reference["crs"], + "count": 1, + "dtype": "uint8", + "nodata": ZERO_NODATA, + "compress": "lzw", + } + ) + + arrays = [] + for result in results: + clipped_array = result["array"] + needs_alignment = ( + clipped_array.shape != (output_meta["height"], output_meta["width"]) + or result["transform"] != output_meta["transform"] + or result["crs"] != output_meta["crs"] + ) + if needs_alignment: + aligned_array = np.zeros( + (output_meta["height"], output_meta["width"]), + dtype=np.int16, + ) + reproject( + source=clipped_array, + destination=aligned_array, + source_transform=result["transform"], + source_crs=result["crs"], + destination_transform=output_meta["transform"], + destination_crs=output_meta["crs"], + src_nodata=ZERO_NODATA, + dst_nodata=ZERO_NODATA, + resampling=Resampling.nearest, + ) + clipped_array = aligned_array + + arrays.append(clipped_array.astype(np.int16, copy=False)) + print( + f"Loaded local LULC raster {result['index']}/{len(raster_paths)}: {result['raster_path']}" + ) + + return arrays, output_meta + + +def _write_change_raster(array, output_path, output_meta): + output_path = os.fspath(output_path) + temp_output_path = f"{output_path}.{os.getpid()}.{uuid.uuid4().hex}.tmp" + raster = np.asarray(array, dtype=np.uint8) + meta = output_meta.copy() + meta.update( + { + "driver": "GTiff", + "count": 1, + "dtype": "uint8", + "nodata": ZERO_NODATA, + "compress": "lzw", + } + ) + try: + with rasterio.open(temp_output_path, "w", **meta) as dst: + dst.write(raster, 1) + os.replace(temp_output_path, output_path) + finally: + if os.path.exists(temp_output_path): + os.remove(temp_output_path) + return str(output_path) + + +def _compute_mode_lulc_three(arrays): + first, second, third = (_as_lulc_int_array(array) for array in arrays) + mode = np.zeros(first.shape, dtype=np.int16) + + same_first = ((first == second) | (first == third)) & (first > 0) + same_second = (second == third) & (second > 0) + mode = np.where(same_first, first, mode) + mode = np.where((mode == 0) & same_second, second, mode) + + unresolved = mode == 0 + if np.any(unresolved): + sentinel = np.int16(np.iinfo(np.int16).max) + positive_stack = np.stack( + [ + np.where(first > 0, first, sentinel), + np.where(second > 0, second, sentinel), + np.where(third > 0, third, sentinel), + ], + axis=0, + ) + fallback = np.min(positive_stack, axis=0) + fallback = np.where(fallback == sentinel, 0, fallback) + mode = np.where(unresolved, fallback, mode) + + return mode + + +def _compute_then_now_modes(remapped_arrays): + if len(remapped_arrays) < 6: + raise ValueError( + "Local change detection requires at least six selected yearly LULC rasters to compare the first three years against the last three years." + ) + then = _compute_mode_lulc_three(remapped_arrays[:3]) + now = _compute_mode_lulc_three(remapped_arrays[-3:]) + return now, then + + +def _compute_built_up_change(lulc_arrays): + remapped_arrays = [_remap_array(array, BUILT_UP_LOOKUP) for array in lulc_arrays] + now, then = _compute_then_now_modes(remapped_arrays) + return _combine_transitions( + then.shape, + [ + (1, (then == 1) & (now == 1)), + (2, (then == 2) & (now == 1)), + (3, (then == 3) & (now == 1)), + (4, (then == 4) & (now == 1)), + ], + ) + + +def _compute_shrub_change(lulc_arrays): + remapped_arrays = [_remap_array(array, SHRUB_LOOKUP) for array in lulc_arrays] + now, then = _compute_then_now_modes(remapped_arrays) + + return _combine_transitions( + then.shape, + [ + (1, (then == 6) & (now == 6)), + (2, (then == 6) & (now == 5)), + (3, (then == 6) & (now == 3)), + (4, (then == 6) & (now == 1)), + (5, (then == 6) & (now == 2)), + (6, (then == 6) & (now == 4)), + ], + ) + + +def _compute_degradation_change(lulc_arrays): + remapped_arrays = [_remap_array(array, DEGRADATION_LOOKUP) for array in lulc_arrays] + now, then = _compute_then_now_modes(remapped_arrays) + return _combine_transitions( + then.shape, + [ + (1, (then == 3) & (now == 3)), + (2, (then == 3) & (now == 1)), + (3, (then == 3) & (now == 5)), + (4, (then == 3) & (now == 6)), + ], + ) + + +def _compute_deforestation_afforestation_modes(lulc_arrays): + remapped_arrays = [ + _remap_array(array, DEFORESTATION_AFFORESTATION_LOOKUP) for array in lulc_arrays + ] + return _compute_then_now_modes(remapped_arrays) + + +def _build_deforestation_change(now, then): + return _combine_transitions( + then.shape, + [ + (1, (then == 3) & (now == 3)), + (2, (then == 3) & (now == 1)), + (3, (then == 3) & (now == 4)), + (4, (then == 3) & (now == 5)), + (5, (then == 3) & (now == 6)), + ], + ) + + +def _compute_deforestation_change(lulc_arrays): + now, then = _compute_deforestation_afforestation_modes(lulc_arrays) + return _build_deforestation_change(now, then) + + +def _build_afforestation_change(now, then): + return _combine_transitions( + then.shape, + [ + (1, (then == 3) & (now == 3)), + (2, (then == 1) & (now == 3)), + (3, (then == 4) & (now == 3)), + (4, (then == 5) & (now == 3)), + (5, (then == 6) & (now == 3)), + ], + ) + + +def _compute_afforestation_change(lulc_arrays): + now, then = _compute_deforestation_afforestation_modes(lulc_arrays) + return _build_afforestation_change(now, then) + + +def _compute_crop_intensity_change(lulc_arrays): + remapped_arrays = [ + _remap_array(array, CROP_INTENSITY_LOOKUP) for array in lulc_arrays + ] + now, then = _compute_then_now_modes(remapped_arrays) + return _combine_transitions( + then.shape, + [ + (1, (then == 6) & (now == 5)), + (2, (then == 7) & (now == 5)), + (3, (then == 7) & (now == 6)), + (4, (then == 5) & (now == 6)), + (5, (then == 5) & (now == 7)), + (6, (then == 6) & (now == 7)), + (7, (then == 5) & (now == 5)), + (8, (then == 6) & (now == 6)), + (9, (then == 7) & (now == 7)), + ], + ) + + +def _compute_single_change_output(param_name, lulc_arrays): + print(f"Computing local change detection raster: {param_name}") + return param_name, globals()[CHANGE_PARAM_FUNCTIONS[param_name]](lulc_arrays) + + +def _compute_forest_change_outputs(lulc_arrays): + print("Computing local change detection raster: Deforestation") + print("Computing local change detection raster: Afforestation") + now, then = _compute_deforestation_afforestation_modes(lulc_arrays) + return { + "Deforestation": _build_deforestation_change(now, then), + "Afforestation": _build_afforestation_change(now, then), + } + + +def _compute_change_outputs(lulc_arrays): + outputs = {} + cpu_workers = min(max(os.cpu_count() or 1, 1), 4) + with ThreadPoolExecutor(max_workers=cpu_workers) as executor: + futures = [ + executor.submit(_compute_single_change_output, param_name, lulc_arrays) + for param_name in ( + "Urbanization", + "Degradation", + "CropIntensity", + "ShrubChange", + ) + ] + futures.append(executor.submit(_compute_forest_change_outputs, lulc_arrays)) + + for future in futures: + result = future.result() + if isinstance(result, dict): + outputs.update(result) + else: + param_name, change_array = result + outputs[param_name] = change_array + + return outputs + + +def run_change_detection_local( + state, + district, + block, + start_year, + end_year, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + push_to_geoserver=True, + sync_layer_metadata=True, +): + state = str(state).strip().lower() + district = str(district).strip().lower() + block = str(block).strip().lower() + start_year = int(start_year) + end_year = int(end_year) + + if end_year < start_year: + raise ValueError("end_year must be greater than or equal to start_year") + + roi_gdf = load_precomputed_roi( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + lulc_raster_paths = _select_change_detection_raster_paths( + start_year=start_year, + end_year=end_year, + ) + lulc_arrays, output_meta = _load_masked_lulc_arrays(roi_gdf, lulc_raster_paths) + change_outputs = _compute_change_outputs(lulc_arrays) + + geoserver_statuses = [] + + for param_name, change_array in change_outputs.items(): + output_stub = _output_stub(district, block, param_name, start_year, end_year) + output_path = build_output_raster_path( + layer_name=output_stub, + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + state=state, + district=district, + block=block, + block_fallback="unknown_block", + ) + raster_path = _write_change_raster( + array=change_array, + output_path=output_path, + output_meta=output_meta, + ) + print(f"Saved local change detection raster: {raster_path}") + + published_layer_name = _published_layer_name(district, block, param_name) + if push_to_geoserver: + upload_res, style_res = push_local_raster_to_geoserver( + file_path=raster_path, + layer_name=published_layer_name, + workspace=GEOSERVER_WORKSPACE, + style_name=param_name.lower(), + ) + print(f"GeoServer upload response for {param_name}: {upload_res}") + print(f"GeoServer style response for {param_name}: {style_res}") + geoserver_statuses.append(True) + + if sync_layer_metadata: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=published_layer_name, + asset_id=raster_path, + dataset_name="Change Detection Raster", + misc={ + "start_year": start_year, + "end_year": end_year, + "is_generated_locally": True, + }, + ) + if layer_id and push_to_geoserver: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + from computing.STAC_specs import generate_STAC_layerwise + + layer_stac_generated = generate_STAC_layerwise.generate_raster_stac( + state=state, + district=district, + block=block, + layer_name=CHANGE_STAC_LAYER_NAMES[param_name], + ) + update_layer_sync_status( + layer_id=layer_id, + is_stac_specs_generated=layer_stac_generated, + ) + + return all(geoserver_statuses) if push_to_geoserver else True + + +def _get_change_detection_local_task( + state, + district, + block, + start_year, + end_year, + gee_account_id=None, +): + _ = gee_account_id + return run_change_detection_local( + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + push_to_geoserver=True, + sync_layer_metadata=True, + ) + + +@app.task(bind=True) +def get_change_detection( + self, + state, + district, + block, + start_year, + end_year, + gee_account_id=None, +): + _ = self + return _get_change_detection_local_task( + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + gee_account_id=gee_account_id, + ) diff --git a/computing/change_detection/change_detection_vector.py b/computing/change_detection/change_detection_vector.py index 95e88837..7e8ceb75 100644 --- a/computing/change_detection/change_detection_vector.py +++ b/computing/change_detection/change_detection_vector.py @@ -40,6 +40,7 @@ def vectorise_change_detection( degradation_vector(roi, state, district, block, start_year, end_year), urbanization_vector(roi, state, district, block, start_year, end_year), crop_intensity_vector(roi, state, district, block, start_year, end_year), + shrub_change_vector(roi, state, district, block, start_year, end_year), ] print(task_list) @@ -52,6 +53,7 @@ def vectorise_change_detection( "Deforestation", "Afforestation", "CropIntensity", + "ShrubChange", ] layer_at_geoserver = False for param in param_list: @@ -137,6 +139,23 @@ def urbanization_vector(roi, state, district, block, start_year, end_year): ) +# Shrub Change +def shrub_change_vector(roi, state, district, block, start_year, end_year): + args = [ + {"value": 1, "label": "sh_sh"}, + {"value": 2, "label": "sh_fa"}, + {"value": 3, "label": "sh_tr"}, + {"value": 4, "label": "sh_bu"}, + {"value": 5, "label": "sh_wa"}, + {"value": 6, "label": "sh_ba"}, + {"value": [2, 3, 4, 5, 6], "label": "total_change"}, + ] # Classes in shrub change raster layer + + return generate_vector( + roi, args, state, district, block, "ShrubChange", start_year, end_year + ) + + # CropnIntensity def crop_intensity_vector(roi, state, district, block, start_year, end_year): @@ -211,13 +230,6 @@ def process_feature(feature): def sync_change_to_geoserver(block, district, state, asset_id, param, layer_id): - # stac_spec_layer_name_dict = { - # "Urbanization": "change_urbanization_vector", - # "Degradation": "change_cropping_reduction_vector", - # "Deforestation": "change_tree_cover_loss_vector", - # "Afforestation": "change_tree_cover_gain_vector", - # "CropIntensity": "change_cropping_intensity_vector", - # } fc = ee.FeatureCollection(asset_id).getInfo() fc = {"features": fc["features"], "type": fc["type"]} res = sync_layer_to_geoserver( diff --git a/computing/change_detection/change_detection_vector_local.py b/computing/change_detection/change_detection_vector_local.py new file mode 100644 index 00000000..8c84c912 --- /dev/null +++ b/computing/change_detection/change_detection_vector_local.py @@ -0,0 +1,244 @@ +import os + +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text + +from computing.config_loader import ( + CHANGE_DETECTION_RASTER_OUTPUT_DIR as CHANGE_RASTER_OUTPUT_BASE_DIR, + CHANGE_DETECTION_VECTOR_OUTPUT_DIR as LOCAL_OUTPUT_BASE_DIR, +) +from computing.local_compute_helper import ( + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + build_output_raster_path, + build_output_vector_path, + compute_categorical_raster_areas_for_watersheds, + ensure_file_exists, + load_precomputed_watersheds, + push_local_vector_to_geoserver, + write_vector_output, +) +from computing.utils import ( + save_layer_info_to_db, + update_layer_sync_status, +) + +GEOSERVER_WORKSPACE = "change_detection" + +CHANGE_VECTOR_CLASS_DEFINITIONS = { + "Afforestation": [ + {"value": 1, "label": "fo_fo"}, + {"value": 2, "label": "bu_fo"}, + {"value": 3, "label": "fa_fo"}, + {"value": 4, "label": "ba_fo"}, + {"value": 5, "label": "sc_fo"}, + {"value": [2, 3, 4, 5], "label": "total_aff"}, + ], + "Deforestation": [ + {"value": 1, "label": "fo_fo"}, + {"value": 2, "label": "fo_bu"}, + {"value": 3, "label": "fo_fa"}, + {"value": 4, "label": "fo_ba"}, + {"value": 5, "label": "fo_sc"}, + {"value": [2, 3, 4, 5], "label": "total_def"}, + ], + "Degradation": [ + {"value": 1, "label": "f_f"}, + {"value": 2, "label": "f_bu"}, + {"value": 3, "label": "f_ba"}, + {"value": 4, "label": "f_sc"}, + {"value": [2, 3, 4], "label": "total_deg"}, + ], + "Urbanization": [ + {"value": 1, "label": "bu_bu"}, + {"value": 2, "label": "w_bu"}, + {"value": 3, "label": "tr_bu"}, + {"value": 4, "label": "b_bu"}, + {"value": [2, 3, 4], "label": "total_urb"}, + ], + "CropIntensity": [ + {"value": 1, "label": "do_si"}, + {"value": 2, "label": "tr_si"}, + {"value": 3, "label": "tr_do"}, + {"value": 4, "label": "si_do"}, + {"value": 5, "label": "si_tr"}, + {"value": 6, "label": "do_tr"}, + {"value": 7, "label": "si_si"}, + {"value": 8, "label": "do_do"}, + {"value": 9, "label": "tr_tr"}, + {"value": [1, 2, 3, 4, 5, 6], "label": "total_change"}, + ], + "ShrubChange": [ + {"value": 1, "label": "sh_sh"}, + {"value": 2, "label": "sh_fa"}, + {"value": 3, "label": "sh_tr"}, + {"value": 4, "label": "sh_bu"}, + {"value": 5, "label": "sh_wa"}, + {"value": 6, "label": "sh_ba"}, + {"value": [2, 3, 4, 5, 6], "label": "total_change"}, + ], +} + + +def _slug(value, fallback): + return valid_gee_text(str(value).strip().lower()) or fallback + + +def _published_layer_name(district, block, param_name): + return ( + f"change_vector_{_slug(district, 'unknown_district')}_" + f"{_slug(block, 'unknown_block')}_{param_name}" + ) + + +def _output_stub(district, block, param_name, start_year, end_year): + return ( + f"{_published_layer_name(district, block, param_name)}_{start_year}_{end_year}" + ) + + +def _resolve_local_change_raster_path( + state, district, block, param_name, start_year, end_year +): + raster_path = build_output_raster_path( + layer_name=( + f"change_{_slug(district, 'unknown_district')}_" + f"{_slug(block, 'unknown_block')}_{param_name}_{int(start_year)}_{int(end_year)}" + ), + output_base_dir=CHANGE_RASTER_OUTPUT_BASE_DIR, + state=state, + district=district, + block=block, + block_fallback="unknown_block", + ) + ensure_file_exists(raster_path, f"Local change detection raster for {param_name}") + return str(raster_path) + + +def run_change_detection_vector_local( + state, + district, + block, + start_year, + end_year, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + push_to_geoserver=True, + sync_layer_metadata=True, +): + state = str(state).strip().lower() + district = str(district).strip().lower() + block = str(block).strip().lower() + start_year = int(start_year) + end_year = int(end_year) + + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + + geoserver_statuses = [] + + for param_name, class_definitions in CHANGE_VECTOR_CLASS_DEFINITIONS.items(): + raster_path = _resolve_local_change_raster_path( + state=state, + district=district, + block=block, + param_name=param_name, + start_year=start_year, + end_year=end_year, + ) + print(f"Using local change raster for {param_name}: {raster_path}") + + result_gdf = compute_categorical_raster_areas_for_watersheds( + watersheds_gdf=watersheds_gdf, + raster_path=raster_path, + class_definitions=class_definitions, + ) + + published_layer_name = _published_layer_name(district, block, param_name) + output_stub = _output_stub(district, block, param_name, start_year, end_year) + output_path = build_output_vector_path( + layer_name=output_stub, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + block_fallback="unknown_block", + ) + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=published_layer_name, + ) + print(f"Saved local change detection vector: {asset_id}") + if push_to_geoserver: + geoserver_response = push_local_vector_to_geoserver( + path=os.path.splitext(asset_id)[0], + layer_name=published_layer_name, + workspace=GEOSERVER_WORKSPACE, + file_type="gpkg", + ) + print(f"GeoServer response for {param_name}: {geoserver_response}") + if not isinstance(geoserver_response, dict) or geoserver_response.get( + "status_code" + ) not in (200, 201): + return False + geoserver_statuses.append(True) + + if sync_layer_metadata: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=published_layer_name, + asset_id=asset_id, + dataset_name="Change Detection Vector", + misc={"is_generated_locally": True}, + ) + if layer_id and push_to_geoserver: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + + return all(geoserver_statuses) if push_to_geoserver else True + + +def _vectorise_change_detection_local_task( + state, + district, + block, + start_year, + end_year, + gee_account_id=None, +): + _ = gee_account_id + return run_change_detection_vector_local( + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + push_to_geoserver=True, + sync_layer_metadata=True, + ) + + +@app.task(bind=True) +def vectorise_change_detection( + self, + state, + district, + block, + start_year, + end_year, + gee_account_id=None, +): + _ = self + return _vectorise_change_detection_local_task( + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + gee_account_id=gee_account_id, + ) diff --git a/computing/config.yaml b/computing/config.yaml new file mode 100644 index 00000000..d1d1b46d --- /dev/null +++ b/computing/config.yaml @@ -0,0 +1,622 @@ +# Local compute manifest. + +base_layers: + static_layers: + - name: terrain + local_path: "{DATA_DIR}/base_layers/terrain_raster_fabdam_pan_india.tif" + source: s3://corestack-datasets/base_layers/static_layers/terrain/terrain_raster_fabdam_pan_india.tif + type: file + + - name: slope percentage + local_path: "{DATA_DIR}/base_layers/slope_percentage/slope_percentage.tif" + source: s3://corestack-datasets/base_layers/static_layers/slope_percentage/slope_percentage.tif + type: file + + - name: Aquifer Layer + aliases: + - aquifer + local_path: "{DATA_DIR}/base_layers/aquifer/aquifer.geojson" + source: s3://corestack-datasets/base_layers/static_layers/aquifer/aquifer.geojson + type: file + + - name: surface water bodies + aliases: + - swb + - pan india waterbodies + local_path: "{DATA_DIR}/base_layers/pan_india_waterbodies.fgb" + source: "" + type: file + + - name: restoration opportunity + local_path: "{DATA_DIR}/base_layers/restoration_opportunity/restoration_opportunity.tif" + source: s3://corestack-datasets/base_layers/static_layers/restoration_opportunity/restoration_opportunity.tif + type: file + + - name: soge + local_path: "{DATA_DIR}/base_layers/soge/soge.geojson" + source: s3://corestack-datasets/base_layers/static_layers/soge/soge.geojson + type: file + + - name: lcw + local_path: "{DATA_DIR}/base_layers/lcw/lcw.geojson" + source: s3://corestack-datasets/base_layers/static_layers/lcw/lcw.geojson + type: file + + - name: factory csr + local_path: "{DATA_DIR}/base_layers/factory_csr/factory_csr.geojson" + source: s3://corestack-datasets/base_layers/static_layers/factory_csr/factory_csr.geojson + type: file + + - name: mining + local_path: "{DATA_DIR}/base_layers/mining/mining.geojson" + source: s3://corestack-datasets/base_layers/static_layers/mining/mining.geojson + type: file + + - name: facilities + local_path: "{DATA_DIR}/base_layers/facilities/facilities.geojson" + source: s3://corestack-datasets/base_layers/static_layers/facilities/facilities.geojson + type: file + + - name: dem + local_path: "{DATA_DIR}/base_layers/dem/dem.tif" + source: s3://corestack-datasets/base_layers/static_layers/dem/dem.tif + type: file + + - name: river + local_path: "{DATA_DIR}/base_layers/river/river.geojson" + source: s3://corestack-datasets/base_layers/static_layers/river/river.geojson + type: file + + - name: canal + local_path: "{DATA_DIR}/base_layers/canal/canal.geojson" + source: s3://corestack-datasets/base_layers/static_layers/canal/canal.geojson + type: file + + - name: hydrological soil group + local_path: "{DATA_DIR}/base_layers/hydrological_soil_group/hydrological_soil_group.geojson" + source: s3://corestack-datasets/base_layers/static_layers/hydrological_soil_group/hydrological_soil_group.geojson + type: file + + - name: mission antyodaya + local_path: "{DATA_DIR}/base_layers/mission_antyodaya/mission_antyodaya.gpkg" + source: "" + type: file + + - name: ceew climate data + local_path: "{DATA_DIR}/base_layers/ceew_climate_data/ceew_climate_data.tif" + source: "" + type: file + + - name: water quality aikosh + local_path: "{DATA_DIR}/base_layers/water_quality_aikosh/water_quality_aikosh.geojson" + source: "" + type: file + + - name: groundwater quality + local_path: "{DATA_DIR}/base_layers/groundwater_quality/groundwater_quality.geojson" + source: "" + type: file + + - name: admin boundaries + local_path: "{DATA_DIR}/admin-boundary/input/soi_tehsil.geojson" + source: "" + # Full admin-boundary archive (~8 GB, 7z) containing this file; extracted in place. + gdrive_id: 1VqIhB6HrKFDkDnlk1vedcEHhh5fk4f1d + type: file + + - name: MWS Boundaries + aliases: + - mws + local_path: "{DATA_DIR}/base_layers/static_layers/mws/Microwatershed_v2_with_details.geojson" + source: s3://corestack-datasets/base_layers/static_layers/mws/Microwatershed_v2_with_details.geojson + type: file + + - name: stream order + local_path: "{DATA_DIR}/base_layers/stream_order/stream_order.tif" + source: "" + type: file + + - name: drainage lines + local_path: "{DATA_DIR}/base_layers/drainage_lines_pan_india.gpkg" + source: "" + type: file + + - name: natural depression + local_path: "{DATA_DIR}/base_layers/natural_depression/natural_depression.tif" + source: "" + type: file + + - name: catchment area + local_path: "{DATA_DIR}/base_layers/catchment_area/catchment_area.tif" + source: "" + type: file + + - name: soil_health + local_path: "{DATA_DIR}/base_layers/soil_health/soil_health.tif" + source: "" + type: file + + - name: soil_type + local_path: "{DATA_DIR}/base_layers/soil_type/" + source: "" + type: directory + + - name: Nrega Layer (Scrapping) + aliases: + - nrega layer + local_path: "{DATA_DIR}/base_layers/nrega/nrega.gpkg" + source: "" + type: file + + - name: tree health + local_path: "{DATA_DIR}/base_layers/tree_health/tree_health.tif" + source: "" + type: file + + - name: green credit + local_path: "{DATA_DIR}/base_layers/green_credit/green_credit.geojson" + source: "" + type: file + + - name: Agroecological Natural Farming + aliases: + - aez + local_path: "{DATA_DIR}/base_layers/AEZ_GeoJSON.geojson" + source: s3://corestack-datasets/base_layers/AEZs/Agro_Ecological_Regions.shp + type: file + + periodic_layers: + - name: lulc_v3 + filename: lulc_v3_{year}_{year+1}.tif + local_path: "{DATA_DIR}/base_layers/lulc/{filename}" + periodicity: annual + start_year: 2017 + end_year: 2024 + source: s3://corestack-datasets/base_layers/periodic_layers/lulc/lulc_v3/{filename} + type: file + + - name: lulc_v4 + filename: lulc_v4_{year}_{year+1}.tif + local_path: "{DATA_DIR}/base_layers/lulc/{filename}" + periodicity: annual + start_year: 2017 + end_year: 2024 + source: s3://corestack-datasets/base_layers/periodic_layers/lulc/lulc_v4/{filename} + type: file + + - name: lulc_v4_local_compute_cold_start + local_path: "{DATA_DIR}/base_layers/periodic/lulc_v4_local_compute_cold_start/" + source: "" + type: file + + - name: lulc_v4_sampling_cold_start + local_path: "{DATA_DIR}/base_layers/periodic/lulc_v4_sampling_cold_start/" + source: "" + type: file + + - name: lulc_v2_river_basin + local_path: "{DATA_DIR}/base_layers/periodic/lulc_v2_river_basin/" + source: "" + type: file + + - name: lulc_v3_river_basin + local_path: "{DATA_DIR}/base_layers/periodic/lulc_v3_river_basin/" + source: "" + type: file + + - name: lulc_for_tehsil + local_path: "{DATA_DIR}/base_layers/periodic/lulc_for_tehsil/" + source: "" + type: file + + - name: lulc_v4_aez_cold_start + local_path: "{DATA_DIR}/base_layers/periodic/lulc_v4_aez_cold_start/" + source: "" + type: file + + - name: lulc_v4_temporal_correction_cold_start + local_path: "{DATA_DIR}/base_layers/periodic/lulc_v4_temporal_correction_cold_start/" + source: "" + type: file + + - name: lulc_v4_aez_after_6_years + local_path: "{DATA_DIR}/base_layers/periodic/lulc_v4_aez_after_6_years/" + source: "" + type: file + + - name: lulc_v4_temporal_correction_after_6_years + local_path: "{DATA_DIR}/base_layers/periodic/lulc_v4_temporal_correction_after_6_years/" + source: "" + type: file + + - name: lulc_v4_local_compute_adding_new_grids + local_path: "{DATA_DIR}/base_layers/periodic/lulc_v4_local_compute_adding_new_grids/" + source: "" + type: file + + - name: lulc_v4_sampling_adding_new_grids + local_path: "{DATA_DIR}/base_layers/periodic/lulc_v4_sampling_adding_new_grids/" + source: "" + type: file + + - name: lulc_v4_aez_adding_new_grids + local_path: "{DATA_DIR}/base_layers/periodic/lulc_v4_aez_adding_new_grids/" + source: "" + type: file + + - name: lulc_v4_temporal_correction_adding_new_grids + local_path: "{DATA_DIR}/base_layers/periodic/lulc_v4_temporal_correction_adding_new_grids/" + source: "" + type: file + + - name: lulc_v4_temporal_correction_adding_new_grids_after_6_years + local_path: "{DATA_DIR}/base_layers/periodic/lulc_v4_temporal_correction_adding_new_grids_after_6_years/" + source: "" + type: file + + - name: rainfall jaxa + local_path: "{DATA_DIR}/base_layers/periodic/rainfall_jaxa/" + source: "" + type: file + + - name: rainfall chirps + local_path: "{DATA_DIR}/base_layers/periodic/rainfall_chirps/" + source: "" + type: file + + - name: et fldas + local_path: "{DATA_DIR}/base_layers/periodic/et_fldas/" + source: "" + type: file + + - name: et gldas + local_path: "{DATA_DIR}/base_layers/periodic/et_gldas/" + source: "" + type: file + + - name: pet + local_path: "{DATA_DIR}/base_layers/periodic/pet/" + source: "" + type: file + + - name: dw lulc + local_path: "{DATA_DIR}/base_layers/periodic/dw_lulc/" + source: "" + type: file + + - name: runoff + local_path: "{DATA_DIR}/base_layers/periodic/runoff/" + source: "" + type: file + + - name: swb + local_path: "{DATA_DIR}/base_layers/periodic/swb/" + source: "" + type: file + + - name: drought spei + local_path: "{DATA_DIR}/base_layers/periodic/drought_spei/" + source: "" + type: file + + - name: forest resistance and resilience to drought + local_path: "{DATA_DIR}/base_layers/periodic/forest_resistance_resilience_drought/" + source: "" + type: file + + - name: extreme rainfall + local_path: "{DATA_DIR}/base_layers/periodic/extreme_rainfall/" + source: "" + type: file + + - name: forest resistance and resilience to extreme rainfall + local_path: "{DATA_DIR}/base_layers/periodic/forest_resistance_resilience_extreme_rainfall/" + source: "" + type: file + + tehsil_level: + on_demand_layers: + - name: lulc_farm_boundary + local_path: "{DATA_DIR}/base_layers/on_demand/lulc_farm_boundary/" + source: "" + type: file + + - name: generate_ponds + local_path: "{DATA_DIR}/base_layers/on_demand/ponds/" + source: "" + type: file + + - name: generate_wells + local_path: "{DATA_DIR}/base_layers/on_demand/wells/" + source: "" + type: file + + - name: forest additonality + aliases: + - forest additionality + local_path: "{DATA_DIR}/base_layers/on_demand/forest_additionality/" + source: "" + type: file + gee_only: + - name: fes_clart_layer + local_path: "{DATA_DIR}/base_layers/on_demand/fes_clart_layer/" + source: "" + type: file + + - name: generate_clart + local_path: "{DATA_DIR}/base_layers/on_demand/clart/" + source: "" + type: file + + - name: generate_ndvi_timeseries + local_path: "{DATA_DIR}/base_layers/on_demand/ndvi_timeseries/" + source: "" + type: file + + - name: downscaling et + local_path: "{DATA_DIR}/base_layers/on_demand/downscaling_et/" + source: "" + type: file + + - name: forest fire + local_path: "{DATA_DIR}/base_layers/on_demand/forest_fire/" + source: "" + type: file + + - name: grassland degradation + local_path: "{DATA_DIR}/base_layers/on_demand/grassland_degradation/" + source: "" + type: file + + - name: deforestation on forest fringes core + local_path: "{DATA_DIR}/base_layers/on_demand/deforestation_forest_fringes_core/" + source: "" + type: file + + - name: plantation_site_suitability + local_path: "{DATA_DIR}/base_layers/on_demand/plantation_site_suitability/" + source: "" + type: file + + - name: temperature and humidity fortnightly timeseries + local_path: "{DATA_DIR}/base_layers/on_demand/temperature_humidity_fortnightly/" + source: "" + type: file + + - name: pollution through satellite measured aerosol density + local_path: "{DATA_DIR}/base_layers/on_demand/aerosol_density/" + source: "" + type: file +derived_layers: + - name: generate_mws_layer + + - name: lulc v3 + filename: "{district}_{block}_{start_year}-07-01_{end_year}-06-30_LULCmap_10m.tif" + local_path: "{DATA_DIR}/lulc/lulc_v3_local/{state}/{district}/{block}/{filename}" + geoserver_workspace: LULC_v3 + layer_type: raster + + - name: lulc v4 + + - name: generate_ci_layer + + - name: generate_terrain_raster + + - name: generate_terrain_descriptor + + - name: terrain_lulc_slope_cluster + aliases: + - lulc slope clusters + filename: "{district}_{block}_lulc_slope.gpkg" + local_path: "{DATA_DIR}/lulc_X_terrain/lulc_slope_clusters_local/{state}/{district}/{block}/{filename}" + geoserver_workspace: terrain_lulc + layer_type: vector + + - name: terrain_lulc_plain_cluster + aliases: + - lulc plain clusters + filename: "{district}_{block}_lulc_plain.gpkg" + local_path: "{DATA_DIR}/lulc_X_terrain/lulc_plain_clusters_local/{state}/{district}/{block}/{filename}" + geoserver_workspace: terrain_lulc + layer_type: vector + + - name: generate_terrain_compute_all + + - name: lulc vector + filename: lulc_vector_{district}_{block}.gpkg + local_path: "{DATA_DIR}/lulc/lulc_vector_local/{state}/{district}/{block}/{filename}" + geoserver_workspace: lulc_vector + layer_type: vector + + - name: change detection + params: + [ + Afforestation, + Deforestation, + Degradation, + Urbanization, + CropIntensity, + ] + filename: change_{district}_{block}_{param_name}_{start_year}_{end_year}.tif + local_path: "{DATA_DIR}/change_detection/change_detection_local/{state}/{district}/{block}/{filename}" + geoserver_workspace: change_detection + layer_type: raster + + - name: change detection vector + params: + [ + Afforestation, + Deforestation, + Degradation, + Urbanization, + CropIntensity, + ] + filename: change_vector_{district}_{block}_{param_name}_{start_year}_{end_year}.gpkg + local_path: "{DATA_DIR}/change_detection/change_detection_vector_local/{state}/{district}/{block}/{filename}" + geoserver_workspace: change_detection + layer_type: vector + + - name: aquifer vector + local_path: "{DATA_DIR}/layers/aquifer_vector/" + geoserver_workspace: aquifer_vector + layer_type: vector + + - name: soil type + filename: soil_type_{district}_{block}.gpkg + local_path: "{DATA_DIR}/layers/soil_type/{state}/{district}/{block}/{filename}" + geoserver_workspace: soil_type + layer_type: vector + + - name: generate_swb + local_path: "{DATA_DIR}/surface_water_bodies/" + geoserver_workspace: surface_water_bodies + layer_type: vector + + - name: crop_grid + + - name: dem_raster + local_path: "{DATA_DIR}/fabdem/fabdem_local/" + geoserver_workspace: dem + layer_type: raster + + - name: dem_vector + + - name: drainage_density_vector + local_path: "{DATA_DIR}/drainage_density/" + geoserver_workspace: drainage_density + layer_type: vector + + - name: generate_canal_vector + local_path: "{DATA_DIR}/canal/canal_local/" + geoserver_workspace: canal + layer_type: vector + + - name: generate_river_data + local_path: "{DATA_DIR}/river/river_local/" + geoserver_workspace: river + layer_type: vector + + - name: run_off_daily_and_fortnightly + + - name: clip_soil_health + + - name: soil_health_vector + + - name: tree_health_raster + + - name: tree_health_vector + + - name: stream_order + + - name: hydrology_fortnightly + + - name: hydrology_annual + + - name: generate_block_layer + + - name: generate_drainage_layer + local_path: "{DATA_DIR}/layers/drainage_lines/drainage_lines_local/" + geoserver_workspace: drainage_lines + layer_type: vector + + - name: restoration_opportunity + local_path: "{DATA_DIR}/layers/restoration_opportunity/" + geoserver_workspace: restoration + layer_type: raster + + - name: soge_vector + local_path: "{DATA_DIR}/layers/SOGE_vector/" + geoserver_workspace: soge + layer_type: vector + + - name: generate_lcw + local_path: "{DATA_DIR}/layers/lcw_conflict/" + geoserver_workspace: lcw + layer_type: raster + + - name: generate_agroecological + local_path: "{DATA_DIR}/layers/agroecological/" + geoserver_workspace: agroecological + layer_type: vector + + - name: generate_factory_csr + local_path: "{DATA_DIR}/layers/factory_csr/" + geoserver_workspace: factory_csr + layer_type: vector + + - name: generate_green_credit + local_path: "{DATA_DIR}/layers/green_credit/" + geoserver_workspace: green_credit + layer_type: vector + + - name: generate_mining + local_path: "{DATA_DIR}/layers/mining/" + geoserver_workspace: mining + layer_type: vector + + - name: generate_natural_depression + local_path: "{DATA_DIR}/layers/natural_depression/" + geoserver_workspace: natural_depression + layer_type: raster + + - name: generate_distance_nearest_DL + local_path: "{DATA_DIR}/layers/distance_nearest_upstream_DL/" + geoserver_workspace: distance_nearest_upstream_DL + layer_type: raster + + - name: generate_catchment_area_singleflow + local_path: "{DATA_DIR}/layers/catchment_area_singleflow/" + geoserver_workspace: catchment_area_singleflow + layer_type: raster + + - name: generate_slope_percentage + local_path: "{DATA_DIR}/layers/slope_percentage/" + geoserver_workspace: slope_percentage + layer_type: raster + + - name: generate_mws_connectivity_data + local_path: "{DATA_DIR}/layers/mws_connectivity/mws_connectivity_local/" + geoserver_workspace: mws_connectivity + layer_type: vector + + - name: generate_mws_centroid + local_path: "{DATA_DIR}/layers/mws_centroid/" + geoserver_workspace: mws_centroid + layer_type: vector + + - name: generate_facilities_proximity + local_path: "{DATA_DIR}/layers/facilities/" + geoserver_workspace: facilities + layer_type: vector + + - name: generate_nrega_layer + local_path: "{DATA_DIR}/layers/nrega_assets/" + geoserver_workspace: nrega_assets + layer_type: vector + + - name: merge_swb_ponds + + - name: generate_drought_layer + + - name: mws_drought_causality + + - name: clip_drought_spei + + - name: forest resistance and resilience to drought + + - name: extreme rainfall + + - name: forest resistance and resilience to extreme rainfall + + - name: drought sensitivity + + - name: grassland health + + - name: generate_zoi_data + + - name: generate_antyodaya + local_path: "{DATA_DIR}/antyodaya/output/antyodaya_local/" + geoserver_workspace: antyodaya + layer_type: vector + + - name: plantation_site_suitability diff --git a/computing/config_loader.py b/computing/config_loader.py new file mode 100644 index 00000000..0c35461b --- /dev/null +++ b/computing/config_loader.py @@ -0,0 +1,309 @@ +import os +from pathlib import Path + +import yaml + +_CONFIG_PATH = Path(__file__).resolve().parent / "config.yaml" +PROJECT_ROOT = Path(__file__).resolve().parents[1] + +# Root directory for all downloaded/generated local compute data. Defaults to +# `/data` for local development, but should be set to a directory +# mounted from the host (e.g. a Docker volume) in containerized deployments. +DATA_DIR: Path = Path( + os.environ.get("DATA_DIR", str(PROJECT_ROOT / "data")) +).expanduser().resolve() + + +def _load(path: Path) -> dict: + with open(path) as f: + return yaml.safe_load(f) or {} + + +_cfg = _load(_CONFIG_PATH) + + +def _abs(rel_path: str) -> Path: + resolved = rel_path.replace("{DATA_DIR}", str(DATA_DIR)) + base = resolved.split("{")[0].rstrip("/") + return Path(base) + + +def _layer_key(name: str) -> str: + return name.strip().lower().replace(" ", "_").replace("-", "_") + + +def _layer_matches(layer: dict, key: str) -> bool: + names = [layer.get("name", ""), *layer.get("aliases", [])] + return key in {_layer_key(name) for name in names} + + +def _format_periodic_value(template: str, year: int) -> str: + return template.replace("{year+1}", str(year + 1)).replace("{year}", str(year)) + + +def _expand_periodic_layer(layer: dict) -> list[dict]: + if not layer.get("periodicity"): + return [layer] + + if layer.get("periodicity") != "annual": + raise ValueError( + f"Unsupported periodicity for base layer '{layer.get('name')}': " + f"{layer.get('periodicity')}" + ) + + expanded_layers = [] + for year in range(int(layer["start_year"]), int(layer["end_year"]) + 1): + filename = _format_periodic_value(layer["filename"], year) + expanded = dict(layer) + expanded["year"] = year + expanded["filename"] = filename + expanded["local_path"] = _format_periodic_value( + layer["local_path"].replace("{filename}", filename), year + ) + source = layer.get("source") + if source: + expanded["source"] = _format_periodic_value( + source.replace("{filename}", filename), year + ) + expanded_layers.append(expanded) + + return expanded_layers + + +def _expand_manifest_layers(layers: list[dict]) -> list[dict]: + expanded_layers = [] + for layer in layers: + expanded_layers.extend(_expand_periodic_layer(layer)) + return expanded_layers + + +def _walk_manifest_layers(node): + if isinstance(node, list): + yield from _expand_manifest_layers(node) + return + + if not isinstance(node, dict): + return + + for value in node.values(): + yield from _walk_manifest_layers(value) + + +def _manifest_base_layers() -> list[dict]: + base_layers = _cfg.get("base_layers", {}) + return list(_walk_manifest_layers(base_layers)) + + +def _base_layer(name: str, *, required: bool = True) -> dict | None: + key = _layer_key(name) + for layer in _manifest_base_layers(): + if _layer_matches(layer, key): + return layer + if required: + raise KeyError(f"No base layer found in {_CONFIG_PATH.name} for name: {name}") + return None + + +def _base_layer_path( + name: str, + fallback: str | None = None, + allowed_suffixes: tuple[str, ...] | None = None, +) -> Path: + layer = _base_layer(name, required=False) + if layer and layer.get("local_path"): + local_path = _abs(layer["local_path"]) + if not allowed_suffixes or local_path.suffix.lower() in allowed_suffixes: + return local_path + if fallback: + return _abs(fallback) + raise KeyError(f"No local_path found in {_CONFIG_PATH.name} for base layer: {name}") + + +def _derived_layer(name: str) -> dict | None: + key = _layer_key(name) + for layer in _cfg.get("derived_layers", []): + if _layer_matches(layer, key) and layer.get("local_path"): + return layer + return None + + +def _derived_output_dir(name: str) -> Path: + layer = _derived_layer(name) + if not layer: + raise KeyError(f"No derived layer found in {_CONFIG_PATH.name} for name: {name}") + return _abs(layer["local_path"]) + + +# --------------------------------------------------------------------------- +# Input paths +# --------------------------------------------------------------------------- + +LULC_BASE_DIR: Path = _abs( + next( + layer["local_path"] + for layer in _manifest_base_layers() + if layer["local_path"].startswith("{DATA_DIR}/base_layers/lulc/") + ) +).parent + +TERRAIN_RASTER_PATH: Path = _base_layer_path("terrain") + +AEZ_VECTOR_PATH: Path = _base_layer_path("aez") + +PRECOMPUTED_TEHSIL_WATERSHED_DIR: Path = DATA_DIR / "base_layers/tehsil_watersheds" + +MICROWATERSHED_PATH: Path = _base_layer_path("mws") + +AQUIFER_VECTOR_PATH: Path = _base_layer_path( + "aquifer", + fallback="{DATA_DIR}/base_layers/Aquifer_vector.geojson", + allowed_suffixes=(".geojson", ".gpkg", ".shp"), +) + +SWB_VECTOR_PATH: Path = _base_layer_path( + "surface water bodies", + fallback="{DATA_DIR}/base_layers/pan_india_waterbodies.geojson", +) + +SOI_TEHSIL_PATH: Path = _base_layer_path("admin boundaries") + +SOIL_TYPE_BASE_DIR: Path = _base_layer_path("soil type") +SOIL_TYPE_RASTER_PATHS: dict[str, Path] = { + "available_water_capacity": SOIL_TYPE_BASE_DIR / "available_water_capacity.tif", + "soil_drainage_classes": SOIL_TYPE_BASE_DIR / "soil_drainage_classes.tif", + "subsoil_bulk_density": SOIL_TYPE_BASE_DIR / "subsoil_bulk_density.tif", + "subsoil_exchange_capacity": SOIL_TYPE_BASE_DIR + / "subsoil_exchange_capacity.tif", + "subsoil_organic_carbon": SOIL_TYPE_BASE_DIR / "subsoil_organic_carbon.tif", + "subsoil_ph": SOIL_TYPE_BASE_DIR / "subsoil_pH.tif", + "subsoil_texture": SOIL_TYPE_BASE_DIR / "subsoil_texture.tif", + "topsoil_bulk_density": SOIL_TYPE_BASE_DIR / "topsoil_bulk_density.tif", + "topsoil_exchange_capacity": SOIL_TYPE_BASE_DIR + / "topsoil_exchange_capacity.tif", + "topsoil_organic_carbon": SOIL_TYPE_BASE_DIR / "topsoil_organic_carbon.tif", + "topsoil_ph": SOIL_TYPE_BASE_DIR / "topsoil_pH.tif", + "topsoil_texture": SOIL_TYPE_BASE_DIR / "topsoil_texture.tif", +} + +ADMIN_BOUNDARY_INPUT_DIR: Path = DATA_DIR / "admin-boundary/input" +ADMIN_BOUNDARY_OUTPUT_DIR: Path = DATA_DIR / "admin-boundary/output" +VILLAGE_BOUNDARIES_DIR: Path = DATA_DIR / "base_layers/village_boundaries" + +# --------------------------------------------------------------------------- +# Google Drive IDs +# --------------------------------------------------------------------------- + +GDRIVE_ADMIN_BOUNDARY_FILE_ID: str = _base_layer("admin boundaries").get("gdrive_id", "") + +# MWS boundaries are now sourced from S3 (see config.yaml); no Google Drive +# file ID is configured for it anymore. +_mws_layer = _base_layer("mws", required=False) or {} +GDRIVE_MICROWATERSHED_FILE_ID: str = _mws_layer.get("gdrive_id", "") + +# LULC rasters are now sourced from S3 as periodic base layers (see +# config.yaml); no per-year Google Drive file IDs are configured anymore. +LULC_GDRIVE_FILES: list[tuple[str, str]] = [] + +# --------------------------------------------------------------------------- +# Output base directories +# --------------------------------------------------------------------------- + +CHANGE_DETECTION_RASTER_OUTPUT_DIR: Path = _derived_output_dir("change detection") +CHANGE_DETECTION_VECTOR_OUTPUT_DIR: Path = _derived_output_dir( + "change detection vector" +) +LULC_VECTOR_OUTPUT_DIR: Path = _derived_output_dir("lulc vector") +LULC_V3_OUTPUT_DIR: Path = _derived_output_dir("lulc v3") +LULC_SLOPE_CLUSTER_OUTPUT_DIR: Path = _derived_output_dir("lulc slope clusters") +LULC_PLAIN_CLUSTER_OUTPUT_DIR: Path = _derived_output_dir("lulc plain clusters") +AQUIFER_VECTOR_OUTPUT_DIR: Path = _derived_output_dir("aquifer vector") +SWB_VECTOR_OUTPUT_DIR: Path = _derived_output_dir("generate_swb") +SOIL_TYPE_OUTPUT_DIR: Path = _derived_output_dir("soil type") + + +PAN_INDIA_DRAINAGE_LINES_GPKG_PATH = ( + DATA_DIR / "base_layers/drainage_lines_pan_india.gpkg" +) + +PAN_INDIA_DRAINAGE_LINES_PATH = DATA_DIR / "layers/drainage_lines/Pan_India_drainage_lines.gpkg" +LOCAL_DRAINAGE_LINES_OUTPUT = DATA_DIR / "layers/drainage_lines/drainage_lines_local" + +LOCAL_DRAINAGE_DENSITY_OUTPUT = DATA_DIR / "drainage_density" + +PAN_INDIA_CANAL_PATH = _base_layer_path( + "canal", fallback="{DATA_DIR}/canal/Canal_pan_india.geojson" +) +LOCAL_CANAL_OUTPUT = DATA_DIR / "canal/canal_local" + +PAN_INDIA_AGROECOLOGICAL_PATH = _base_layer_path( + "aez", + fallback="{DATA_DIR}/base_layers/Pan_India_agroecological_farming.geojson", +) +LOCAL_AGROECOLOGICAL_OUTPUT = DATA_DIR / "layers/agroecological" + +PAN_INDIA_LCW_PATH = _base_layer_path( + "lcw", fallback="{DATA_DIR}/base_layers/Pan_India_lcw_conflict.geojson" +) +LOCAL_LCW_OUTPUT = DATA_DIR / "layers/lcw_conflict" + +PAN_INDIA_SOGE_PATH = _base_layer_path( + "soge", fallback="{DATA_DIR}/base_layers/Pan_India_SOGE_2020.geojson" +) +LOCAL_SOGE_OUTPUT = DATA_DIR / "layers/SOGE_vector" + +PAN_INDIA_FACTORY_CSR_PATH = _base_layer_path( + "factory csr", fallback="{DATA_DIR}/base_layers/Pan_India_factory_csr.geojson" +) +LOCAL_FACTORY_CSR_OUTPUT = DATA_DIR / "layers/factory_csr" + +PAN_INDIA_GREEN_CREDIT_PATH = DATA_DIR / "base_layers/Pan_India_green_credit.geojson" +LOCAL_GREEN_CREDIT_OUTPUT = DATA_DIR / "layers/green_credit" + +PAN_INDIA_MINING_PATH = _base_layer_path( + "mining", fallback="{DATA_DIR}/base_layers/Pan_India_mining.geojson" +) +LOCAL_MINING_OUTPUT = DATA_DIR / "layers/mining" + +PAN_INDIA_NATURALDEPRESSION_PATH = DATA_DIR / "base_layers/Pan_India_natural_depression.tif" +LOCAL_NATURALDEPRESSION_OUTPUT = DATA_DIR / "layers/natural_depression" + +PAN_INDIA_DISTANCETONEARESTDRAINAGE_PATH = DATA_DIR / "base_layers/Pan_India_distance_to_nearest_drainage.tif" +LOCAL_DISTANCETONEARESTDRAINAGE_OUTPUT = DATA_DIR / "layers/distance_nearest_upstream_DL" + +PAN_INDIA_FACILITIES_PATH = _base_layer_path( + "facilities", fallback="{DATA_DIR}/base_layers/Pan_India_facilities_polygon.geojson" +) +LOCAL_FACILITIES_OUTPUT = DATA_DIR / "layers/facilities" +PAN_INDIA_CATCHMENT_AREA_PATH = DATA_DIR / "base_layers/Pan_India_catchment_area.tif" +LOCAL_CATCHMENT_AREA_OUTPUT = DATA_DIR / "layers/catchment_area_singleflow" + +PAN_INDIA_SLOPE_PERCENTAGE_PATH = _base_layer_path( + "slope percentage", fallback="{DATA_DIR}/base_layers/Pan_India_slope_percentage.tif" +) +LOCAL_SLOPE_PERCENTAGE_OUTPUT = DATA_DIR / "layers/slope_percentage" + +PAN_INDIA_MWS_CONNECTIVITY_PATH = DATA_DIR / "layers/mws_connectivity/Pan_India_mws_connectivity.geojson" +LOCAL_MWS_CONNECTIVITY_OUTPUT = DATA_DIR / "layers/mws_connectivity/mws_connectivity_local" + +LOCAL_MWS_CENTROID_OUTPUT = DATA_DIR / "layers/mws_centroid" + +NREGA_LOCAL_OUTPUT = DATA_DIR / "layers/nrega_assets" +PAN_INDIA_RESTORATION_PATH = _base_layer_path( + "restoration opportunity", + fallback="{DATA_DIR}/base_layers/Pan_India_WRI_Restoration.tif", +) +LOCAL_RESTORATION_OUTPUT = DATA_DIR / "layers/restoration_opportunity" + +PAN_INDIA_RIVER_PATH = _base_layer_path( + "river", fallback="{DATA_DIR}/river/River_pan_india.geojson" +) +LOCAL_RIVER_OUTPUT = DATA_DIR / "river/river_local" + +PAN_INDIA_FABDEM_PATH = _base_layer_path("dem", fallback="{DATA_DIR}/fabdem/fabdem_pan_india.tif") +LOCAL_FABDEM_OUTPUT = DATA_DIR / "fabdem/fabdem_local" + +PAN_INDIA_ANTYODAYA_2020 = DATA_DIR / "base_layers/pan_india_antyodaya_2020.gpkg" +LOCAL_ANTYODAYA_2020_OUTPUT = DATA_DIR / "antyodaya/output/antyodaya_local" + +PAN_INDIA_LIVESTOCKS = DATA_DIR / "base_layers/pan_india_livestock.gpkg" +LOCAL_LIVESTOCKS_OUTPUT = DATA_DIR / "livestock/output/livestock_local" diff --git a/computing/cropping_intensity/cropping_intesity_local.py b/computing/cropping_intensity/cropping_intesity_local.py new file mode 100644 index 00000000..5b53b587 --- /dev/null +++ b/computing/cropping_intensity/cropping_intesity_local.py @@ -0,0 +1,311 @@ +import os + +import pandas as pd +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text + +from computing.local_compute_helper import ( + LULC_BASE_DIR, + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + PROJECT_ROOT, + build_output_vector_path, + compute_categorical_raster_areas_for_watersheds, + compute_union_categorical_area_across_rasters_for_watersheds, + load_precomputed_watersheds, + resolve_lulc_raster_paths, + write_vector_output, +) +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) + + +LOCAL_OUTPUT_BASE_DIR = ( + PROJECT_ROOT / "data/cropping_intensity/cropping_intensity_local" +) +GEOSERVER_WORKSPACE = "crop_intensity" +LOCAL_ALGORITHM = "local_cropping_intensity" +LOCAL_ALGORITHM_VERSION = "local-1.0" +INITIAL_YEAR = 2017 + +SINGLE_KHARIF = 8 +SINGLE_NON_KHARIF = 9 +DOUBLE = 10 +TRIPLE = 11 + +YEARLY_CLASS_DEFINITIONS = ( + {"value": SINGLE_KHARIF, "label_prefix": "single_kharif_cropped_area_"}, + {"value": SINGLE_NON_KHARIF, "label_prefix": "single_non_kharif_cropped_area_"}, + {"value": DOUBLE, "label_prefix": "doubly_cropped_area_"}, + {"value": TRIPLE, "label_prefix": "triply_cropped_area_"}, +) + + +def _slug(value, fallback): + return valid_gee_text(str(value).strip().lower()) or fallback + + +def _layer_name(asset_suffix, zoi_ci_asset=False): + if zoi_ci_asset: + return f"{asset_suffix}_intensity_ZOI" + return f"{asset_suffix}_intensity" + + +def _resolve_asset_suffix(state, district, block, asset_suffix): + if asset_suffix: + return asset_suffix + if state and district and block: + return ( + f"{_slug(district, 'unknown_district')}_" + f"{_slug(block, 'unknown_block')}" + ) + raise ValueError("state, district, and block are required for local cropping intensity.") + + +def _coerce_year(year_value, label): + try: + return int(year_value) + except (TypeError, ValueError) as exc: + raise ValueError(f"{label} must be a valid integer year.") from exc + + +def _compute_yearly_area_columns(result_gdf, raster_paths, start_year): + for year, raster_path in zip(range(start_year, start_year + len(raster_paths)), raster_paths): + print(f"Computing local cropping intensity inputs for {year}-{year + 1}: {raster_path}") + year_definitions = [ + { + "value": class_definition["value"], + "label": f"{class_definition['label_prefix']}{year}", + } + for class_definition in YEARLY_CLASS_DEFINITIONS + ] + year_result = compute_categorical_raster_areas_for_watersheds( + watersheds_gdf=result_gdf, + raster_path=raster_path, + class_definitions=year_definitions, + ) + for definition in year_definitions: + result_gdf[definition["label"]] = year_result[definition["label"]].astype( + float + ) + result_gdf[f"single_cropped_area_{year}"] = ( + result_gdf[f"single_kharif_cropped_area_{year}"] + + result_gdf[f"single_non_kharif_cropped_area_{year}"] + ) + return result_gdf + + +def _compute_total_croppable_area(result_gdf, denominator_raster_paths, end_year): + output_column = f"total_cropable_area_ever_hydroyear_{INITIAL_YEAR}_{end_year}" + print( + "Computing local ever-croppable denominator " + f"for {INITIAL_YEAR}-{end_year} using {len(denominator_raster_paths)} rasters" + ) + return compute_union_categorical_area_across_rasters_for_watersheds( + watersheds_gdf=result_gdf, + raster_paths=denominator_raster_paths, + class_values=[SINGLE_KHARIF, SINGLE_NON_KHARIF, DOUBLE, TRIPLE], + output_column=output_column, + ) + + +def _compute_cropping_intensity_columns(result_gdf, start_year, end_year): + denominator_column = f"total_cropable_area_ever_hydroyear_{INITIAL_YEAR}_{end_year}" + denominator = pd.to_numeric(result_gdf[denominator_column], errors="coerce").fillna(0.0) + + for year in range(start_year, end_year + 1): + single_area = pd.to_numeric( + result_gdf[f"single_cropped_area_{year}"], errors="coerce" + ).fillna(0.0) + double_area = pd.to_numeric( + result_gdf[f"doubly_cropped_area_{year}"], errors="coerce" + ).fillna(0.0) + triple_area = pd.to_numeric( + result_gdf[f"triply_cropped_area_{year}"], errors="coerce" + ).fillna(0.0) + + nonzero_denominator = denominator > 0 + intensity = pd.Series(0.0, index=result_gdf.index, dtype=float) + intensity.loc[nonzero_denominator] = ( + single_area.loc[nonzero_denominator] / denominator.loc[nonzero_denominator] + + 2.0 * double_area.loc[nonzero_denominator] / denominator.loc[nonzero_denominator] + + 3.0 * triple_area.loc[nonzero_denominator] / denominator.loc[nonzero_denominator] + ) + result_gdf[f"cropping_intensity_{year}"] = intensity.astype(float) + + return result_gdf + + +def run_cropping_intensity_local( + state, + district, + block, + start_year, + end_year, + asset_suffix=None, + zoi_ci_asset=False, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + lulc_dir=LULC_BASE_DIR, + push_to_geoserver=True, + sync_layer_metadata=True, +): + state = str(state).strip().lower() + district = str(district).strip().lower() + block = str(block).strip().lower() + start_year = _coerce_year(start_year, "start_year") + end_year = _coerce_year(end_year, "end_year") + + if start_year > end_year: + raise ValueError("start_year cannot be greater than end_year") + if start_year < INITIAL_YEAR: + raise ValueError( + f"start_year must be greater than or equal to {INITIAL_YEAR} for local cropping intensity." + ) + + asset_suffix = _resolve_asset_suffix(state, district, block, asset_suffix) + layer_name = _layer_name(asset_suffix, zoi_ci_asset=zoi_ci_asset) + + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + + yearly_raster_paths = resolve_lulc_raster_paths( + start_year=start_year, + end_year=end_year, + lulc_dir=lulc_dir, + ) + denominator_raster_paths = resolve_lulc_raster_paths( + start_year=INITIAL_YEAR, + end_year=end_year, + lulc_dir=lulc_dir, + ) + + result_gdf = watersheds_gdf.copy() + result_gdf = _compute_yearly_area_columns( + result_gdf=result_gdf, + raster_paths=yearly_raster_paths, + start_year=start_year, + ) + result_gdf = _compute_total_croppable_area( + result_gdf=result_gdf, + denominator_raster_paths=denominator_raster_paths, + end_year=end_year, + ) + result_gdf = _compute_cropping_intensity_columns( + result_gdf=result_gdf, + start_year=start_year, + end_year=end_year, + ) + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + block_fallback="unknown_block", + ) + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local cropping intensity vector: {asset_id}") + + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer response: {geoserver_response}") + if not isinstance(geoserver_response, dict) or geoserver_response.get( + "status_code" + ) not in (200, 201): + return False + + if sync_layer_metadata: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Cropping Intensity", + misc={ + "start_year": start_year, + "end_year": end_year, + "is_generated_locally": True, + }, + algorithm=LOCAL_ALGORITHM, + algorithm_version=LOCAL_ALGORITHM_VERSION, + ) + if layer_id and push_to_geoserver: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + + return True + + +def _generate_cropping_intensity_local_task( + state=None, + district=None, + block=None, + roi_path=None, + asset_suffix=None, + asset_folder_list=None, + app_type="MWS", + start_year=None, + end_year=None, + gee_account_id=None, + zoi_ci_asset=None, +): + _ = roi_path, asset_folder_list, app_type, gee_account_id + return run_cropping_intensity_local( + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + asset_suffix=asset_suffix, + zoi_ci_asset=bool(zoi_ci_asset), + push_to_geoserver=True, + sync_layer_metadata=True, + ) + + +@app.task(bind=True) +def generate_cropping_intensity( + self, + state=None, + district=None, + block=None, + roi_path=None, + asset_suffix=None, + asset_folder_list=None, + app_type="MWS", + start_year=None, + end_year=None, + gee_account_id=None, + zoi_ci_asset=None, +): + _ = self + return _generate_cropping_intensity_local_task( + state=state, + district=district, + block=block, + roi_path=roi_path, + asset_suffix=asset_suffix, + asset_folder_list=asset_folder_list, + app_type=app_type, + start_year=start_year, + end_year=end_year, + gee_account_id=gee_account_id, + zoi_ci_asset=zoi_ci_asset, + ) diff --git a/computing/forest_fire/forest_fire.py b/computing/forest_fire/forest_fire.py index 15c9199f..74d9f5f0 100644 --- a/computing/forest_fire/forest_fire.py +++ b/computing/forest_fire/forest_fire.py @@ -1,292 +1,255 @@ -""" -Forest Fire pipeline. - -Generates a vector layer with MODIS-based fire metrics per micro-watershed. -Uses MODIS Terra (MOD14A1) and Aqua (MYD14A1) active fire products to -quantify fire radiative power and fire frequency across a user-defined -time window. - -For each MWS the pipeline computes four metrics: - - fire_frp_sum_per_year – yearly-normalised total Fire Radiative Power - - fire_frp_mean – temporal mean FRP - - fire_frp_max – peak FRP observed - - fire_count_per_year – yearly-normalised fire pixel count -""" - -import ee -from computing.utils import ( - sync_fc_to_geoserver, - save_layer_info_to_db, - update_layer_sync_status, -) -from utilities.constants import GEE_PATHS -from utilities.gee_utils import ( - ee_initialize, - check_task_status, - valid_gee_text, - get_gee_dir_path, - is_gee_asset_exists, - make_asset_public, - export_vector_asset_to_gee, -) -from nrm_app.celery import app -from .forest_fire_utils import ( - SCALE, - load_fire_collections, - prepare_frp_images, -) - - -@app.task(bind=True) -def generate_forest_fire_layer( - self, - state, - district, - block, - start_year=2001, - end_year=2022, - gee_account_id=None, - app_type="MWS", -): - """ - Generate MODIS fire-risk metrics as a vector layer. - - For each micro-watershed the task computes four fire metrics from - merged MODIS Terra + Aqua active fire products, exports the result - as a vector asset to GEE, syncs to GeoServer, and saves metadata. - - Args: - state: str – state name. - district: str – district name. - block: str – block / tehsil name. - start_year: int – first year of the analysis window (default 2001). - end_year: int – last year of the analysis window (default 2022). - gee_account_id: int – GEE service-account ID for authentication. - app_type: str – application type key in GEE_PATHS (default "MWS"). - """ - - # ------------------------------------------------------------------ - # STEP 1: Initialize GEE and set up paths - # ------------------------------------------------------------------ - ee_initialize(gee_account_id) - - start_year = int(start_year) - end_year = int(end_year) - n_years = end_year - start_year + 1 - - asset_suffix = ( - valid_gee_text(district.lower()) + "_" + valid_gee_text(block.lower()) - ) - asset_folder_list = [state, district, block] - - description = f"forest_fire_{asset_suffix}_{start_year}_{end_year}" - layer_name = f"{asset_suffix}_forest_fire" - - asset_id = ( - get_gee_dir_path( - asset_folder_list, - asset_path=GEE_PATHS[app_type]["GEE_ASSET_PATH"], - ) - + description - ) - - print(f"Forest Fire pipeline started: {asset_id=}") - - # ------------------------------------------------------------------ - # STEP 2: Set up ROI (MWS boundaries from GEE) - # ------------------------------------------------------------------ - roi_path = ( - get_gee_dir_path( - asset_folder_list, - asset_path=GEE_PATHS[app_type]["GEE_ASSET_PATH"], - ) - + f"filtered_mws_{valid_gee_text(district.lower())}" - + f"_{valid_gee_text(block.lower())}_uid" - ) - mws_fc = ee.FeatureCollection(roi_path) - - # Add this debug block before the map to identify bad features - print("Total features:", mws_fc.size().getInfo()) - - # Check for features whose geometry area is 0 - def flag_bad_geom(f): - area = f.geometry().area(1) - return f.set("area_m2", area) - - debug_fc = mws_fc.map(flag_bad_geom) - bad = debug_fc.filter(ee.Filter.eq("area_m2", 0)) - print("Bad geometry count:", bad.size().getInfo()) - print("Bad UIDs:", bad.aggregate_array("uid").getInfo()) - - # ------------------------------------------------------------------ - # STEP 3: Compute fire metrics - # ------------------------------------------------------------------ - if not is_gee_asset_exists(asset_id): - - # Prepare temporally-aggregated fire images - frp_collection = load_fire_collections(start_year, end_year) - fire_images = prepare_frp_images(frp_collection, n_years) - - frp_sum_img = fire_images["sum"] - frp_mean_img = fire_images["mean"] - frp_max_img = fire_images["max"] - fire_count_img = fire_images["count"] - # ---- map compute over all MWS features ---- - mws_fc = mws_fc.filter(ee.Filter.notNull(["uid"])) - - # After your area filter, add geometry repair - def repair_geometry(f): - return f.setGeometry(f.geometry().buffer(0).simplify(10)) - - def validate_feature(f): - geom = f.geometry() - - return f.set({"geom_type": geom.type(), "area_m2": geom.area(1)}) - - validated = mws_fc.map(validate_feature) - - mws_fc = validated.filter(ee.Filter.gt("area_m2", 0)) - mws_fc = mws_fc.map(repair_geometry) - - fire_projection = ee.Image(frp_collection.first()).select("MaxFRP").projection() - metric_images = { - "fire_frp_sum_per_year": frp_sum_img.rename( - "fire_frp_sum_per_year" - ).setDefaultProjection(fire_projection), - "fire_frp_mean": frp_mean_img.rename("fire_frp_mean").setDefaultProjection( - fire_projection - ), - "fire_frp_max": frp_max_img.rename("fire_frp_max").setDefaultProjection( - fire_projection - ), - "fire_count_per_year": fire_count_img.rename( - "fire_count_per_year" - ).setDefaultProjection(fire_projection), - } - - fc = _reduce_fire_metric( - mws_fc, - metric_images["fire_frp_sum_per_year"], - ee.Reducer.sum(), - "fire_frp_sum_per_year", - fire_projection, - ) - fc = _reduce_fire_metric( - fc, - metric_images["fire_frp_mean"], - ee.Reducer.mean(), - "fire_frp_mean", - fire_projection, - ) - fc = _reduce_fire_metric( - fc, - metric_images["fire_frp_max"], - ee.Reducer.mean(), - "fire_frp_max", - fire_projection, - ) - fc = _reduce_fire_metric( - fc, - metric_images["fire_count_per_year"], - ee.Reducer.sum(), - "fire_count_per_year", - fire_projection, - ) - - fc = fc.select( - [ - "uid", - "fire_frp_sum_per_year", - "fire_frp_mean", - "fire_frp_max", - "fire_count_per_year", - ] - ) - - # -------------------------------------------------------------- - # STEP 4: Export to GEE - # -------------------------------------------------------------- - task_id = export_vector_asset_to_gee(fc, description, asset_id) - if task_id: - check_task_status([task_id]) - print("Forest Fire layer exported to GEE.") - - # ------------------------------------------------------------------ - # STEP 5: Publish to GeoServer and save metadata to DB - # ------------------------------------------------------------------ - layer_at_geoserver = _save_to_db_and_sync_to_geoserver( - layer_name=layer_name, - asset_id=asset_id, - start_year=start_year, - end_year=end_year, - asset_suffix=asset_suffix, - state=state, - district=district, - block=block, - ) - return layer_at_geoserver - - -def _reduce_fire_metric(fc, image, reducer, metric_name, projection): - reduced = image.reduceRegions( - collection=fc, - reducer=reducer, - scale=SCALE, - crs=projection, - tileScale=4, - ) - - def fill_null(feature): - value = feature.get(metric_name) - value = ee.Algorithms.If(ee.Algorithms.IsEqual(value, None), 0, value) - return feature.set(metric_name, ee.Number(value)) - - return reduced.map(fill_null) - - -# ------------------------------------------------------------------ -# Private helpers (publish / persist) -# ------------------------------------------------------------------ - - -def _save_to_db_and_sync_to_geoserver( - layer_name=None, - asset_id=None, - start_year=None, - end_year=None, - asset_suffix=None, - state=None, - district=None, - block=None, -): - """Publish asset to GeoServer and persist metadata to the database.""" - print("Forest Fire: save_to_db_and_sync_to_geoserver") - - layer_id = None - if state and district and block: - layer_id = save_layer_info_to_db( - state=state, - district=district, - block=block, - layer_name=layer_name, - asset_id=asset_id, - dataset_name="Forest Fire", - misc={ - "start_year": start_year, - "end_year": end_year, - }, - ) - - make_asset_public(asset_id) - - fc = ee.FeatureCollection(asset_id) - res = sync_fc_to_geoserver(fc, asset_suffix, layer_name, "forest_fire") - print(res) - - layer_at_geoserver = False - if res["status_code"] == 201 and layer_id: - update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) - print("Forest Fire: sync to geoserver flag updated") - layer_at_geoserver = True - - return layer_at_geoserver +import ee + +# import geemap + +from .forest_fire_utils import ( + SCALE, + MAXPIX, + load_fire_collections, + prepare_frp_images, +) +from utilities.constants import GEE_PATHS +from utilities.gee_utils import ( + ee_initialize, + export_vector_asset_to_gee, + get_gee_dir_path, + check_task_status, + valid_gee_text, + is_gee_asset_exists, + make_asset_public, +) +from computing.utils import ( + sync_fc_to_geoserver, + update_layer_sync_status, + save_layer_info_to_db, +) +from nrm_app.celery import app + + +@app.task(bind=True) +def generate_forest_fire_layer( + self, + state, + district, + block, + start_year=2001, + end_year=2022, + gee_account_id=None, + app_type="MWS", +): + """ + Generate MODIS fire metrics as a vector layer for each MWS feature. + """ + + ee_initialize(gee_account_id) + + start_year = int(start_year) + end_year = int(end_year) + if end_year < start_year: + raise ValueError("end_year must be greater than or equal to start_year") + + n_years = end_year - start_year + 1 + asset_suffix = ( + valid_gee_text(district.lower()) + "_" + valid_gee_text(block.lower()) + ) + asset_folder_list = [state, district, block] + + gee_base_path = get_gee_dir_path( + asset_folder_list, + asset_path=GEE_PATHS[app_type]["GEE_ASSET_PATH"], + ) + description = f"forest_fire_{asset_suffix}_{start_year}_{end_year}" + layer_name = f"{asset_suffix}_forest_fire" + asset_id = gee_base_path + description + + print(f"Forest Fire updated pipeline started: {asset_id=}") + + # ------------------------------------------------------------------ + # STEP 2: Set up ROI (MWS boundaries from GEE) + # ------------------------------------------------------------------ + roi_path = ( + get_gee_dir_path( + asset_folder_list, + asset_path=GEE_PATHS[app_type]["GEE_ASSET_PATH"], + ) + + f"filtered_mws_{valid_gee_text(district.lower())}" + + f"_{valid_gee_text(block.lower())}_uid" + ) + mws_fc = ee.FeatureCollection(roi_path) + + print("Total features:", mws_fc.size().getInfo()) + + def flag_bad_geom(f): + area = f.geometry().area(1) + return f.set("area_m2", area) + + debug_fc = mws_fc.map(flag_bad_geom) + bad = debug_fc.filter(ee.Filter.eq("area_m2", 0)) + + print("Bad geometry count:", bad.size().getInfo()) + print("Bad UIDs:", bad.aggregate_array("uid").getInfo()) + + # ------------------------------------------------------------------ + # STEP 3: Compute fire metrics + # ------------------------------------------------------------------ + if not is_gee_asset_exists(asset_id): + frp_collection = load_fire_collections(start_year, end_year) + fire_images = prepare_frp_images(frp_collection, n_years) + + frp_sum_img = fire_images["sum"] + frp_mean_img = fire_images["mean"] + frp_max_img = fire_images["max"] + fire_count_img = fire_images["count"] + + def compute_fire_metrics(f): + geom = f.geometry() + + def reduce(img, reducer, band): + val = img.reduceRegion( + reducer=reducer, + geometry=geom, + scale=SCALE, + maxPixels=MAXPIX, + bestEffort=True, + ).get(band) + + return ee.Number( + ee.Algorithms.If( + ee.Algorithms.IsEqual(val, None), + 0, + val, + ) + ) + + fire_frp_sum_per_year = reduce( + frp_sum_img, + ee.Reducer.sum(), + "MaxFRP", + ) + fire_frp_mean = reduce( + frp_mean_img, + ee.Reducer.mean(), + "MaxFRP", + ) + fire_frp_max = reduce( + frp_max_img, + ee.Reducer.mean(), + "MaxFRP", + ) + fire_count_per_year = reduce( + fire_count_img, + ee.Reducer.sum(), + "fire", + ) + + return ( + f.set("fire_frp_sum_per_year", fire_frp_sum_per_year) + .set("fire_frp_mean", fire_frp_mean) + .set("fire_frp_max", fire_frp_max) + .set("fire_count_per_year", fire_count_per_year) + ) + + def repair_geometry(f): + return f.setGeometry(f.geometry().buffer(0).simplify(10)) + + def validate_feature(f): + geom = f.geometry() + + return f.set( + { + "geom_type": geom.type(), + "area_m2": geom.area(1), + } + ) + + validated = mws_fc.map(validate_feature) + + mws_fc = validated.filter(ee.Filter.gt("area_m2", 0)) + + mws_fc = mws_fc.map(repair_geometry) + + fc = mws_fc.map(compute_fire_metrics) + + fc = fc.select( + [ + "uid", + "fire_frp_sum_per_year", + "fire_frp_mean", + "fire_frp_max", + "fire_count_per_year", + ] + ) + + # ------------------------------------------------------------------ + # STEP 4 + # ------------------------------------------------------------------ + + # print("Exporting locally...") + # + # geemap.ee_export_vector( + # fc, + # filename=f"{description}.geojson", + # ) + + task_id = export_vector_asset_to_gee(fc, description, asset_id) + if task_id: + check_task_status([task_id]) + print("Forest Fire layer exported to GEE.") + + # ------------------------------------------------------------------ + # STEP 5: Publish to GeoServer and save metadata to DB + # ------------------------------------------------------------------ + layer_at_geoserver = _save_to_db_and_sync_to_geoserver( + layer_name=layer_name, + asset_id=asset_id, + start_year=start_year, + end_year=end_year, + asset_suffix=asset_suffix, + state=state, + district=district, + block=block, + ) + return layer_at_geoserver + + +def _save_to_db_and_sync_to_geoserver( + layer_name=None, + asset_id=None, + start_year=None, + end_year=None, + asset_suffix=None, + state=None, + district=None, + block=None, +): + """Publish asset to GeoServer and persist metadata to the database.""" + print("Forest Fire: save_to_db_and_sync_to_geoserver") + + layer_id = None + if state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Forest Fire", + misc={ + "start_year": start_year, + "end_year": end_year, + }, + ) + + make_asset_public(asset_id) + + fc = ee.FeatureCollection(asset_id) + res = sync_fc_to_geoserver(fc, asset_suffix, layer_name, "forest_fire") + print(res) + + layer_at_geoserver = False + if res["status_code"] == 201 and layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Forest Fire: sync to geoserver flag updated") + layer_at_geoserver = True + + return layer_at_geoserver diff --git a/computing/forest_fire/forest_fire_updated.py b/computing/forest_fire/forest_fire_updated.py deleted file mode 100644 index ae8c3c14..00000000 --- a/computing/forest_fire/forest_fire_updated.py +++ /dev/null @@ -1,263 +0,0 @@ -""" -Clean Forest Fire pipeline. - -Uses MODIS Terra and Aqua active fire products to compute fire incident -metrics for each micro-watershed. -""" - -import ee - -from computing.utils import ( - save_layer_info_to_db, - sync_fc_to_geoserver, - update_layer_sync_status, -) -from nrm_app.celery import app -from utilities.constants import GEE_PATHS -from utilities.gee_utils import ( - check_task_status, - ee_initialize, - export_vector_asset_to_gee, - get_gee_dir_path, - is_gee_asset_exists, - make_asset_public, - valid_gee_text, -) - -SCALE = 1000 -TILE_SCALE = 4 - -TERRA_FIRE_PATH = "MODIS/061/MOD14A1" -AQUA_FIRE_PATH = "MODIS/061/MYD14A1" -FIRE_BAND = "MaxFRP" - -METRIC_FIELDS = [ - "uid", - "fire_frp_sum_per_year", - "fire_frp_mean", - "fire_frp_max", - "fire_count_per_year", -] - - -@app.task(bind=True) -def generate_forest_fire_layer_updated( - self, - state, - district, - block, - start_year=2001, - end_year=2022, - gee_account_id=None, - app_type="MWS", -): - """ - Generate MODIS fire metrics as a vector layer for each MWS feature. - """ - - ee_initialize(gee_account_id) - - start_year = int(start_year) - end_year = int(end_year) - if end_year < start_year: - raise ValueError("end_year must be greater than or equal to start_year") - - n_years = end_year - start_year + 1 - asset_suffix = ( - valid_gee_text(district.lower()) + "_" + valid_gee_text(block.lower()) - ) - asset_folder_list = [state, district, block] - - gee_base_path = get_gee_dir_path( - asset_folder_list, - asset_path=GEE_PATHS[app_type]["GEE_ASSET_PATH"], - ) - description = f"forest_fire_{asset_suffix}_{start_year}_{end_year}" - layer_name = f"{asset_suffix}_forest_fire" - asset_id = gee_base_path + description - - print(f"Forest Fire updated pipeline started: {asset_id=}") - - if not is_gee_asset_exists(asset_id): - roi_path = ( - gee_base_path - + f"filtered_mws_{valid_gee_text(district.lower())}" - + f"_{valid_gee_text(block.lower())}_uid" - ) - mws_fc = _prepare_mws_features(roi_path) - - fire_collection = _load_fire_collection(start_year, end_year) - fire_count = fire_collection.size().getInfo() - print("Forest Fire MODIS image count:", fire_count) - if fire_count == 0: - raise ValueError( - "No MODIS fire images found for " - f"{start_year}-01-01 to {end_year}-12-31" - ) - - projection = ee.Image(fire_collection.first()).select(FIRE_BAND).projection() - metric_images = _build_metric_images(fire_collection, n_years, projection) - - fc = _add_fire_metrics(mws_fc, metric_images, projection) - fc = fc.select(METRIC_FIELDS) - - task_id = export_vector_asset_to_gee(fc, description, asset_id) - if task_id: - check_task_status([task_id]) - print("Forest Fire updated layer exported to GEE.") - - return _save_to_db_and_sync_to_geoserver( - layer_name=layer_name, - asset_id=asset_id, - start_year=start_year, - end_year=end_year, - asset_suffix=asset_suffix, - state=state, - district=district, - block=block, - ) - - -def _load_fire_collection(start_year, end_year): - start_date = f"{start_year}-07-01" - end_date = f"{end_year}-07-01" - - terra = ee.ImageCollection(TERRA_FIRE_PATH) - aqua = ee.ImageCollection(AQUA_FIRE_PATH) - - return terra.merge(aqua).filterDate(start_date, end_date).select(FIRE_BAND) - - -def _prepare_mws_features(roi_path): - fc = ee.FeatureCollection(roi_path).filter(ee.Filter.notNull(["uid"])) - - def repair_and_measure(feature): - geom = feature.geometry().buffer(0).simplify(10) - return feature.setGeometry(geom).set("area_m2", geom.area(1)) - - return fc.map(repair_and_measure).filter(ee.Filter.gt("area_m2", 0)) - - -def _build_metric_images(fire_collection, n_years, projection): - def mask_fire_pixels(image): - image = ee.Image(image) - return image.updateMask(image.gt(0)) - - def fire_incident_pixel(image): - image = ee.Image(image) - return image.gt(0).unmask(0) - - fire_only = fire_collection.map(mask_fire_pixels) - fire_incidents = fire_collection.map(fire_incident_pixel) - - def prepare(image, metric_name): - return ee.Image(image).rename(metric_name).setDefaultProjection(projection) - - return { - "fire_frp_sum_per_year": prepare( - fire_only.sum().divide(n_years), - "fire_frp_sum_per_year", - ), - "fire_frp_mean": prepare( - fire_only.mean(), - "fire_frp_mean", - ), - "fire_frp_max": prepare( - fire_only.max(), - "fire_frp_max", - ), - "fire_count_per_year": prepare( - fire_incidents.sum().divide(n_years), - "fire_count_per_year", - ), - } - - -def _add_fire_metrics(mws_fc, metric_images, projection): - fc = _reduce_metric( - mws_fc, - metric_images["fire_frp_sum_per_year"], - ee.Reducer.sum(), - "fire_frp_sum_per_year", - projection, - ) - fc = _reduce_metric( - fc, - metric_images["fire_frp_mean"], - ee.Reducer.mean(), - "fire_frp_mean", - projection, - ) - fc = _reduce_metric( - fc, - metric_images["fire_frp_max"], - ee.Reducer.mean(), - "fire_frp_max", - projection, - ) - return _reduce_metric( - fc, - metric_images["fire_count_per_year"], - ee.Reducer.sum(), - "fire_count_per_year", - projection, - ) - - -def _reduce_metric(fc, image, reducer, metric_name, projection): - reduced = image.reduceRegions( - collection=fc, - reducer=reducer, - scale=SCALE, - crs=projection, - tileScale=TILE_SCALE, - ) - - def fill_null(feature): - value = feature.get(metric_name) - value = ee.Algorithms.If(ee.Algorithms.IsEqual(value, None), 0, value) - return feature.set(metric_name, ee.Number(value)) - - return reduced.map(fill_null) - - -def _save_to_db_and_sync_to_geoserver( - layer_name=None, - asset_id=None, - start_year=None, - end_year=None, - asset_suffix=None, - state=None, - district=None, - block=None, -): - print("Forest Fire updated: save_to_db_and_sync_to_geoserver") - - layer_id = None - if state and district and block: - layer_id = save_layer_info_to_db( - state=state, - district=district, - block=block, - layer_name=layer_name, - asset_id=asset_id, - dataset_name="Forest Fire", - misc={ - "start_year": start_year, - "end_year": end_year, - }, - ) - - make_asset_public(asset_id) - - fc = ee.FeatureCollection(asset_id) - res = sync_fc_to_geoserver(fc, asset_suffix, layer_name, "forest_fire") - print(res) - - layer_at_geoserver = False - if res["status_code"] == 201 and layer_id: - update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) - print("Forest Fire updated: sync to geoserver flag updated") - layer_at_geoserver = True - - return layer_at_geoserver diff --git a/computing/forest_fire/forest_fire_utils.py b/computing/forest_fire/forest_fire_utils.py index d54ccd68..3632c1a4 100644 --- a/computing/forest_fire/forest_fire_utils.py +++ b/computing/forest_fire/forest_fire_utils.py @@ -1,77 +1,76 @@ -""" -Utility functions and constants for the Forest Fire pipeline. - -Provides MODIS fire data loading, FRP preprocessing, and fire-binary -helpers for per-MWS fire risk analysis using Google Earth Engine. -""" - -import ee - -# ---------------------------------------- -# PARAMETERS / CONSTANTS -# ---------------------------------------- - -SCALE = 1000 -MAXPIX = 1e13 - -# MODIS Active Fire products (Terra + Aqua) -TERRA_FIRE_PATH = "MODIS/061/MOD14A1" -AQUA_FIRE_PATH = "MODIS/061/MYD14A1" - - -def load_fire_collections(start_year, end_year): - """ - Load and merge MODIS Terra + Aqua active fire collections. - - Filters the merged collection to the date range - [start_year-01-01, end_year-12-31] and selects the MaxFRP band. - - Args: - start_year: int – first year of the analysis window. - end_year: int – last year of the analysis window. - - Returns: - ee.ImageCollection – merged, date-filtered MaxFRP collection. - """ - start_date = f"{start_year}-07-01" - end_date = f"{end_year + 1}-07-01" - - terra = ee.ImageCollection(TERRA_FIRE_PATH) - aqua = ee.ImageCollection(AQUA_FIRE_PATH) - - fires = terra.merge(aqua).filterDate(start_date, end_date) - return fires.select("MaxFRP") - - -def prepare_frp_images(frp_collection, n_years): - """ - Pre-compute the four temporally aggregated fire images. - - Args: - frp_collection: ee.ImageCollection – MaxFRP collection. - n_years: int – number of years in the analysis window. - - Returns: - dict with keys 'sum', 'mean', 'max', 'count', each mapping to - an ee.Image ready for spatial reduction. - """ - - # Mask zeros for FRP statistics - def mask_fire(img): - return img.updateMask(img.gt(0)) - - frp_masked = frp_collection.map(mask_fire) - - # Binary fire occurrence for count - def fire_binary(img): - return img.gt(0).unmask(0).rename("fire") - - fire_binary_collection = frp_collection.map(fire_binary) - - return { - "sum": frp_masked.sum().divide(n_years), # yearly-normalised total FRP - "mean": frp_masked.mean(), # temporal mean FRP - # "max": frp_masked.max(), # peak FRP - "max": frp_masked.max(), - "count": fire_binary_collection.sum().divide(n_years), # yearly fire frequency - } +""" +Utility functions and constants for the Forest Fire pipeline. + +Provides MODIS fire data loading, FRP preprocessing, and fire-binary +helpers for per-MWS fire risk analysis using Google Earth Engine. +""" + +import ee + +# ---------------------------------------- +# PARAMETERS / CONSTANTS +# ---------------------------------------- + +SCALE = 1000 +MAXPIX = 1e13 + +# MODIS Active Fire products (Terra + Aqua) +TERRA_FIRE_PATH = "MODIS/061/MOD14A1" +AQUA_FIRE_PATH = "MODIS/061/MYD14A1" + + +def load_fire_collections(start_year, end_year): + """ + Load and merge MODIS Terra + Aqua active fire collections. + + Filters the merged collection to the date range + [start_year-01-01, end_year-12-31] and selects the MaxFRP band. + + Args: + start_year: int – first year of the analysis window. + end_year: int – last year of the analysis window. + + Returns: + ee.ImageCollection – merged, date-filtered MaxFRP collection. + """ + start_date = f"{start_year}-01-01" + end_date = f"{end_year}-12-31" + + terra = ee.ImageCollection(TERRA_FIRE_PATH) + aqua = ee.ImageCollection(AQUA_FIRE_PATH) + + fires = terra.merge(aqua).filterDate(start_date, end_date) + return fires.select("MaxFRP") + + +def prepare_frp_images(frp_collection, n_years): + """ + Pre-compute the four temporally aggregated fire images. + + Args: + frp_collection: ee.ImageCollection – MaxFRP collection. + n_years: int – number of years in the analysis window. + + Returns: + dict with keys 'sum', 'mean', 'max', 'count', each mapping to + an ee.Image ready for spatial reduction. + """ + + # Mask zeros for FRP statistics + def mask_fire(img): + return img.updateMask(img.gt(0)) + + frp_masked = frp_collection.map(mask_fire) + + # Binary fire occurrence for count + def fire_binary(img): + return img.gt(0).unmask(0).rename("fire") + + fire_binary_collection = frp_collection.map(fire_binary) + + return { + "sum": frp_masked.sum().divide(n_years), # yearly-normalised total FRP + "mean": frp_masked.mean(), # temporal mean FRP + "max": frp_masked.max(), + "count": fire_binary_collection.sum().divide(n_years), # yearly fire frequency + } diff --git a/computing/forest_fire/forest_fire_utils_old.py b/computing/forest_fire/forest_fire_utils_old.py new file mode 100644 index 00000000..d54ccd68 --- /dev/null +++ b/computing/forest_fire/forest_fire_utils_old.py @@ -0,0 +1,77 @@ +""" +Utility functions and constants for the Forest Fire pipeline. + +Provides MODIS fire data loading, FRP preprocessing, and fire-binary +helpers for per-MWS fire risk analysis using Google Earth Engine. +""" + +import ee + +# ---------------------------------------- +# PARAMETERS / CONSTANTS +# ---------------------------------------- + +SCALE = 1000 +MAXPIX = 1e13 + +# MODIS Active Fire products (Terra + Aqua) +TERRA_FIRE_PATH = "MODIS/061/MOD14A1" +AQUA_FIRE_PATH = "MODIS/061/MYD14A1" + + +def load_fire_collections(start_year, end_year): + """ + Load and merge MODIS Terra + Aqua active fire collections. + + Filters the merged collection to the date range + [start_year-01-01, end_year-12-31] and selects the MaxFRP band. + + Args: + start_year: int – first year of the analysis window. + end_year: int – last year of the analysis window. + + Returns: + ee.ImageCollection – merged, date-filtered MaxFRP collection. + """ + start_date = f"{start_year}-07-01" + end_date = f"{end_year + 1}-07-01" + + terra = ee.ImageCollection(TERRA_FIRE_PATH) + aqua = ee.ImageCollection(AQUA_FIRE_PATH) + + fires = terra.merge(aqua).filterDate(start_date, end_date) + return fires.select("MaxFRP") + + +def prepare_frp_images(frp_collection, n_years): + """ + Pre-compute the four temporally aggregated fire images. + + Args: + frp_collection: ee.ImageCollection – MaxFRP collection. + n_years: int – number of years in the analysis window. + + Returns: + dict with keys 'sum', 'mean', 'max', 'count', each mapping to + an ee.Image ready for spatial reduction. + """ + + # Mask zeros for FRP statistics + def mask_fire(img): + return img.updateMask(img.gt(0)) + + frp_masked = frp_collection.map(mask_fire) + + # Binary fire occurrence for count + def fire_binary(img): + return img.gt(0).unmask(0).rename("fire") + + fire_binary_collection = frp_collection.map(fire_binary) + + return { + "sum": frp_masked.sum().divide(n_years), # yearly-normalised total FRP + "mean": frp_masked.mean(), # temporal mean FRP + # "max": frp_masked.max(), # peak FRP + "max": frp_masked.max(), + "count": fire_binary_collection.sum().divide(n_years), # yearly fire frequency + } diff --git a/computing/forest_fringe/forest_fringe.py b/computing/forest_fringe/forest_fringe.py index 08537681..377578a6 100644 --- a/computing/forest_fringe/forest_fringe.py +++ b/computing/forest_fringe/forest_fringe.py @@ -22,7 +22,8 @@ save_layer_info_to_db, update_layer_sync_status, ) -from utilities.constants import GEE_PATHS +from gee_computing.models import GEEAccount +from utilities.constants import GEE_PATHS, AEZ, MWS_DATASET from utilities.gee_utils import ( ee_initialize, check_task_status, @@ -45,14 +46,40 @@ ) +def forest_fringes_on_AEZ(aez_no, gee_account_id=7): + ee_initialize(gee_account_id) + aez = ee.FeatureCollection(AEZ) + mwses = ee.FeatureCollection(MWS_DATASET) + + filter_aez = aez.filter(ee.Filter.eq("ae_regcode", aez_no)) + roi = mwses.filterBounds(filter_aez.geometry()) + + asset_suffix = f"AEZ_{aez_no}" + asset_folder_list = ["forest_fringes"] + generate_forest_fringe_degradation( + roi=roi, + asset_suffix=asset_suffix, + asset_folder_list=asset_folder_list, + gee_account_id=gee_account_id, + app_type="forest_fringes", + sync_to_db=False, + sync_to_geoserver=False, + ) + + @app.task(bind=True) def generate_forest_fringe_degradation( - self, - state, - district, - block, - gee_account_id=None, - app_type="MWS", + self, + state=None, + district=None, + block=None, + roi=None, + asset_suffix=None, + asset_folder_list=None, + gee_account_id=None, + app_type="MWS", + sync_to_db=True, + sync_to_geoserver=True, ): """ Generate forest-fringe metrics as a vector layer. @@ -78,43 +105,38 @@ def generate_forest_fringe_degradation( """ # ------------------------------------------------------------------ - # STEP 1: Initialize GEE and set up paths + # Initialize GEE and set up paths # ------------------------------------------------------------------ ee_initialize(gee_account_id) - asset_suffix = ( + if state and district and block: + asset_suffix = ( valid_gee_text(district.lower()) + "_" + valid_gee_text(block.lower()) - ) - asset_folder_list = [state, district, block] - - description = f"forest_fringe_{asset_suffix}" - layer_name = f"{asset_suffix}_forest_fringe" - - asset_id = ( - get_gee_dir_path( - asset_folder_list, - asset_path=GEE_PATHS[app_type]["GEE_ASSET_PATH"], - ) - + description - ) - - print(f"Forest Fringe pipeline started: {asset_id=}") + ) + asset_folder_list = [state, district, block] - # ------------------------------------------------------------------ - # STEP 2: Set up ROI (MWS boundaries from GEE) - # ------------------------------------------------------------------ - roi_path = ( + roi = ee.FeatureCollection( get_gee_dir_path( asset_folder_list, asset_path=GEE_PATHS[app_type]["GEE_ASSET_PATH"], ) + f"filtered_mws_{valid_gee_text(district.lower())}" + f"_{valid_gee_text(block.lower())}_uid" - ) - mws_fc = ee.FeatureCollection(roi_path) + ) + + description = f"forest_fringe_{asset_suffix}" + if app_type in GEE_PATHS: + asset_path = GEE_PATHS[app_type]["GEE_ASSET_PATH"] + else: + gee_obj = GEEAccount.objects.get(pk=gee_account_id) + asset_path = f"projects/{gee_obj.name}/assets/" + + asset_id = get_gee_dir_path(asset_folder_list, asset_path=asset_path) + description + + print(f"Forest Fringe pipeline started: {asset_id=}") # ------------------------------------------------------------------ - # STEP 3: Compute forest-fringe metrics + # Compute forest-fringe metrics # ------------------------------------------------------------------ if not is_gee_asset_exists(asset_id): @@ -253,7 +275,7 @@ def compute_metrics_per_mws(f): ) # ---- map compute over all MWS features ---- - results_fc = mws_fc.map(compute_metrics_per_mws) + results_fc = roi.map(compute_metrics_per_mws) fc = results_fc.select( [ @@ -271,7 +293,7 @@ def compute_metrics_per_mws(f): ) # -------------------------------------------------------------- - # STEP 4: Export to GEE + # Export to GEE # -------------------------------------------------------------- task_id = export_vector_asset_to_gee(fc, description, asset_id) @@ -280,15 +302,17 @@ def compute_metrics_per_mws(f): print("Forest Fringe layer exported to GEE.") # ------------------------------------------------------------------ - # STEP 5: Publish to GeoServer and save metadata to DB + # Publish to GeoServer and save metadata to DB # ------------------------------------------------------------------ layer_at_geoserver = _save_to_db_and_sync_to_geoserver( - layer_name=layer_name, + layer_name=description, asset_id=asset_id, asset_suffix=asset_suffix, state=state, district=district, block=block, + sync_to_db=sync_to_db, + sync_to_geoserver=sync_to_geoserver, ) return layer_at_geoserver @@ -299,18 +323,20 @@ def compute_metrics_per_mws(f): def _save_to_db_and_sync_to_geoserver( - layer_name=None, - asset_id=None, - asset_suffix=None, - state=None, - district=None, - block=None, + layer_name=None, + asset_id=None, + asset_suffix=None, + state=None, + district=None, + block=None, + sync_to_db=True, + sync_to_geoserver=True, ): """Publish asset to GeoServer and persist metadata to the database.""" print("Forest Fringe: save_to_db_and_sync_to_geoserver") layer_id = None - if state and district and block: + if sync_to_db and state and district and block: layer_id = save_layer_info_to_db( state=state, district=district, @@ -321,15 +347,16 @@ def _save_to_db_and_sync_to_geoserver( ) make_asset_public(asset_id) - - fc = ee.FeatureCollection(asset_id) - res = sync_fc_to_geoserver(fc, asset_suffix, layer_name, "forest_fringes") - print(res) - - layer_at_geoserver = False - if res["status_code"] == 201 and layer_id: - update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) - print("Forest Fringe: sync to geoserver flag updated") - layer_at_geoserver = True - - return layer_at_geoserver + if sync_to_geoserver: + fc = ee.FeatureCollection(asset_id) + res = sync_fc_to_geoserver(fc, asset_suffix, layer_name, "forest_fringes") + print(res) + + layer_at_geoserver = False + if res["status_code"] == 201 and layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Forest Fringe: sync to geoserver flag updated") + layer_at_geoserver = True + + return layer_at_geoserver + return False diff --git a/computing/forest_fringe/forest_fringe_utils.py b/computing/forest_fringe/forest_fringe_utils.py index 04454b0c..e32be959 100644 --- a/computing/forest_fringe/forest_fringe_utils.py +++ b/computing/forest_fringe/forest_fringe_utils.py @@ -6,6 +6,11 @@ """ import ee +from utilities.constants import ( + TREE_OVERALL_CHANGE, + PAN_INDIA_LULC_V3_DATASET, + LTP_STP_CHANGE, +) # ---------------------------------------- # PARAMETERS / CONSTANTS @@ -17,13 +22,6 @@ SCALE = 30 MAXPIX = 1e12 -PAN_INDIA_LULC_PATH = "projects/corestack-datasets/assets/datasets/LULC_v3_river_basin" - -LTP_CHANGE_PATH = "projects/corestack-datasets/assets/datasets/tree_health/final_ltp_stp_change_2017_2021" - -OVERALL_CHANGE_PATH = ( - "projects/corestack-trees/assets/tree_characteristics/overall_change_2017_2023" -) # LULC years used by the forest fringe pipeline LULC_YEARS = [2017, 2018, 2019] @@ -41,7 +39,7 @@ def load_tree_mode(): """ lulc_imgs = ee.ImageCollection( [ - ee.Image(f"{PAN_INDIA_LULC_PATH}/pan_india_lulc_v3_{year}_{year + 1}") + ee.Image(f"{PAN_INDIA_LULC_V3_DATASET}{year}_{year + 1}") .select("predicted_label") .eq(TREE_CLASS) for year in LULC_YEARS @@ -52,12 +50,12 @@ def load_tree_mode(): def load_ltp_change(): """ - Load the LTP/STP change product (2017-2021). + Load the LTP/STP change product Returns: ee.Image – mean of the ltp_stp_change image collection. """ - return ee.ImageCollection(LTP_CHANGE_PATH).mean() + return ee.Image(LTP_STP_CHANGE) def load_overall_change(): @@ -67,7 +65,7 @@ def load_overall_change(): Returns: ee.Image – mean of the overall_change image collection. """ - return ee.ImageCollection(OVERALL_CHANGE_PATH).mean() + return ee.ImageCollection(TREE_OVERALL_CHANGE).mean() def make_fringe(patch, fringe_width=FRINGE_WIDTH): diff --git a/computing/layer_dependency/layer_generation_in_order.py b/computing/layer_dependency/layer_generation_in_order.py index 59bbb7a0..f48fc744 100644 --- a/computing/layer_dependency/layer_generation_in_order.py +++ b/computing/layer_dependency/layer_generation_in_order.py @@ -1,300 +1,731 @@ -from nrm_app.celery import app -from computing.misc.admin_boundary import generate_tehsil_shape_file_data -from computing.misc.nrega import clip_nrega_district_block -from computing.mws.mws import mws_layer -from computing.mws.generate_hydrology import generate_hydrology -from computing.lulc.lulc_v3 import clip_lulc_v3 -from computing.lulc.lulc_vector import vectorise_lulc -from computing.cropping_intensity.cropping_intensity import generate_cropping_intensity -from computing.surface_water_bodies.swb import generate_swb_layer -from computing.drought.drought import calculate_drought -from computing.drought.drought_causality import drought_causality -from computing.crop_grid.crop_grid import create_crop_grids -from computing.change_detection.change_detection import get_change_detection -from computing.change_detection.change_detection_vector import ( - vectorise_change_detection, -) -from computing.misc.restoration_opportunity import generate_restoration_opportunity -from computing.misc.aquifer_vector import generate_aquifer_vector -from computing.terrain_descriptor.terrain_raster import terrain_raster -from computing.terrain_descriptor.terrain_clusters import generate_terrain_clusters -from computing.lulc_X_terrain.lulc_on_plain_cluster import lulc_on_plain_cluster -from computing.lulc_X_terrain.lulc_on_slope_cluster import lulc_on_slope_cluster -from computing.misc.soge_vector import generate_soge_vector -from computing.misc.stream_order import generate_stream_order -from computing.misc.drainage_lines import clip_drainage_lines -from computing.clart.clart import generate_clart_layer -from computing.tree_health.canopy_height import tree_health_ch_raster -from computing.tree_health.canopy_height_vector import tree_health_ch_vector -from computing.tree_health.ccd import tree_health_ccd_raster -from computing.tree_health.ccd_vector import tree_health_ccd_vector -from computing.tree_health.overall_change import tree_health_overall_change_raster -from computing.tree_health.overall_change_vector import ( - tree_health_overall_change_vector, -) -from computing.misc.naturaldepression import generate_natural_depression_data -from computing.misc.distancetonearestdrainage import ( - generate_distance_to_nearest_drainage_line, -) -from computing.misc.catchment_area import generate_catchment_area_singleflow -from computing.misc.slope_percentage import generate_slope_percentage_data -from computing.misc.lcw_conflict import generate_lcw_conflict_data -from computing.misc.agroecological_space import generate_agroecological_data -from computing.misc.factory_csr import generate_factory_csr_data -from computing.misc.green_credit import generate_green_credit_data -from computing.misc.mining_data import generate_mining_data -from computing.plantation.site_suitability import site_suitability -from computing.mws.mws_connectivity import generate_mws_connectivity_data -from computing.misc.ndvi_time_series import ndvi_timeseries -from computing.zoi_layers.zoi import generate_zoi -from computing.mws.mws_centroid import generate_mws_centroid_data -from stats_generator.utils import generate_stats_excel_file -from utilities.gee_utils import valid_gee_text -import os -from nrm_app.celery import app -from computing.models import Layer -import json - -status = {} - - -@app.task(bind=True) -def layer_generate_map( - self, - state, - district, - block, - map_order, - gee_account_id, - start_year=None, - end_year=None, -): - """ - This function take state, district,block and map_order(map to trigger, it can be map_1, map_2_1, map_2_2, map_3, map_4). One map trigger more numbers of pipeline. - """ - # checking:- is mws layer generated? - try: - if map_order in ["map_2_1", "map_2_2", "map_3", "map_4"]: - layer = ( - Layer.objects.filter( - layer_name=f"mws_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}" - ) - .order_by("-layer_version") - .first() - ) - if not layer: - return f"check mws layer for {district}_{block}" - except Exception as e: - return f"exception occur while checking mws for {district}_{block} as: {e}" - - global_args = {} - if start_year: - global_args["start_year"] = start_year - if end_year: - global_args["end_year"] = end_year - - # Load JSON configuration - map_config = load_map_config(map_order) - - if not map_config: - return f"Map configuration not found for {map_order}" - - # triggering map at parent level - for func in map_config: - parent_function = func["name"] - parent_func = globals().get(parent_function) - args = get_args( - iterator_name=func, global_args=global_args, gee_account_id=gee_account_id - ) - deps = func.get("depends_on", []) - run_layer_with_dependency( - deps=deps, - node_func_name=parent_function, - node_func_obj=parent_func, - state=state, - district=district, - block=block, - args=args, - ) - - # triggering map at children level - if status.get(parent_function, False) and "children" in func: - for child in func["children"]: - child_function = child["name"] - child_func = globals().get(child_function) - child_args = get_args( - iterator_name=child, - global_args=global_args, - gee_account_id=gee_account_id, - ) - child_deps = child.get("depends_on", []) - run_layer_with_dependency( - deps=child_deps, - node_func_name=child_function, - node_func_obj=child_func, - state=state, - district=district, - block=block, - args=child_args, - ) - - # triggering map at sub children level - if status.get(child_function, False) and "children" in child: - for sub_child in child["children"]: - sub_child_function = sub_child["name"] - sub_child_func = globals().get(sub_child_function) - sub_child_args = get_args( - iterator_name=sub_child, - global_args=global_args, - gee_account_id=gee_account_id, - ) - sub_child_deps = sub_child.get("depends_on", []) - run_layer_with_dependency( - deps=sub_child_deps, - node_func_name=sub_child_function, - node_func_obj=sub_child_func, - state=state, - district=district, - block=block, - args=sub_child_args, - ) - - return f"{status = }" - - -def load_map_config(map_order): - """ - Load map configuration from JSON file based on map_order. - """ - config_path = os.path.join("data", "layers", "layer_dependency", "layer_map.json") - with open(config_path, "r") as f: - all_configs = json.load(f) - return all_configs.get(map_order, []) - - -def load_end_year_rules(): - """ - Load end year rules from JSON. - """ - config_path = os.path.join( - "data", "layers", "layer_dependency", "end_year_rules.json" - ) - with open(config_path, "r") as f: - return json.load(f) - - -# check the dependency layer is available or not -class DependencyValidator: - - @staticmethod - def clip_lulc_v3(district, block): - return ( - Layer.objects.filter( - layer_name__icontains=f"{valid_gee_text(district)}_{valid_gee_text(block)}_level_" - ).count() - == 24 - ) - - @staticmethod - def terrain_raster(district, block): - return Layer.objects.filter( - layer_name=f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_terrain_raster" - ).exists() - - @staticmethod - def generate_catchment_area_singleflow(district, block): - return Layer.objects.filter( - layer_name=f"catchment_area_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_raster" - ).exists() - - @staticmethod - def generate_stream_order(district, block): - return Layer.objects.filter( - layer_name=f"stream_order_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_vector" - ).exists() - - @staticmethod - def clip_drainage_lines(district, block): - return Layer.objects.filter( - layer_name=f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}", - dataset__name="Drainage", - ).exists() - - @staticmethod - def generate_cropping_intensity(district, block): - return Layer.objects.filter( - layer_name=f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_intensity" - ).exists() - - @staticmethod - def generate_swb_layer(district, block): - return Layer.objects.filter( - layer_name=f"surface_waterbodies_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}" - ).exists() - - @staticmethod - def generate_tehsil_shape_file_data(district, block): - return Layer.objects.filter( - layer_name=f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}", - dataset__name="Admin Boundary", - ).exists() - - -def run_layer_with_dependency( - deps, node_func_name, node_func_obj, state, district, block, args -): - """ - This function checks dependency of layer if it is generated or not and call the pipeline functions and maintain status of each function, - """ - for dep in deps: - checker = getattr(DependencyValidator, dep, None) - status[dep] = checker(district, block) if checker else False - if not status[dep]: - print( - f"Skipping {node_func_name} because dependency {dep} failed or not executed." - ) - status[node_func_name] = False - break - else: - try: - end_year_rules = load_end_year_rules() - if node_func_name in end_year_rules: - args["end_year"] = end_year_rules[node_func_name] - if node_func_name == "site_suitability": - args["project_id"] = None - print( - f"{node_func_name} is running... with args={args, state, district, block}, depends_on={deps}" - ) - if node_func_name == "generate_stats_excel_file": - result = node_func_obj(state, district, block) - else: - result = ( - node_func_obj(state=state, district=district, block=block, **args) - if args - else node_func_obj(state, district, block) - ) - if result: - print(f"{node_func_name} is completed...") - status[node_func_name] = True - else: - print(f"check the {node_func_name}") - status[node_func_name] = False - print(f"{result = }") - except Exception as e: - print(f"{node_func_name} raised an error: {e}") - status[node_func_name] = False - - -def get_args(iterator_name, global_args, gee_account_id): - """ - This function merge the global agrs and local args(define in json maps) return combination of both. - """ - arg = iterator_name.get("args", {}) - args = {"gee_account_id": gee_account_id, **arg} - if iterator_name.get("use_global_args", False): - args = { - **global_args, - "gee_account_id": gee_account_id, - **args, - } - return args +from nrm_app.celery import app +from computing.base_layer_setup import with_tehsil_watershed +from computing.misc.admin_boundary import generate_tehsil_shape_file_data +from computing.misc.nrega import clip_nrega_district_block +from computing.mws.mws import mws_layer +from computing.mws.generate_hydrology import generate_hydrology +from computing.lulc.lulc_v3 import clip_lulc_v3 +from computing.lulc.lulc_vector import vectorise_lulc +from computing.cropping_intensity.cropping_intensity import generate_cropping_intensity +from computing.surface_water_bodies.swb import generate_swb_layer +from computing.drought.drought import calculate_drought +from computing.drought.drought_causality import drought_causality +from computing.crop_grid.crop_grid import create_crop_grids +from computing.change_detection.change_detection import get_change_detection +from computing.change_detection.change_detection_vector import ( + vectorise_change_detection, +) +from computing.misc.restoration_opportunity import generate_restoration_opportunity +from computing.misc.aquifer_vector import generate_aquifer_vector +from computing.terrain_descriptor.terrain_raster import terrain_raster +from computing.terrain_descriptor.terrain_clusters import generate_terrain_clusters +from computing.lulc_X_terrain.lulc_on_plain_cluster import lulc_on_plain_cluster +from computing.lulc_X_terrain.lulc_on_slope_cluster import lulc_on_slope_cluster +from computing.misc.soge_vector import generate_soge_vector +from computing.misc.stream_order import generate_stream_order +from computing.misc.drainage_lines import clip_drainage_lines +from computing.clart.clart import generate_clart_layer +from computing.tree_health.gee.canopy_height import tree_health_ch_raster +from computing.tree_health.gee.canopy_height_vector import tree_health_ch_vector +from computing.tree_health.gee.ccd import tree_health_ccd_raster +from computing.tree_health.gee.ccd_vector import tree_health_ccd_vector +from computing.tree_health.gee.overall_change import tree_health_overall_change_raster +from computing.tree_health.gee.overall_change_vector import ( + tree_health_overall_change_vector, +) +from computing.tree_health.local.canopy_height_local import tree_health_ch_raster_local +from computing.tree_health.local.canopy_height_vector_local import ( + tree_health_ch_vector_local, +) +from computing.tree_health.local.ccd_local import tree_health_ccd_raster_local +from computing.tree_health.local.ccd_vector_local import tree_health_ccd_vector_local +from computing.tree_health.local.overall_change_local import ( + tree_health_overall_change_raster_local, +) +from computing.tree_health.local.overall_change_vector_local import ( + tree_health_overall_change_vector_local, +) +from computing.soil_health.soil_health import soil_health_local +from computing.soil_type.soil_type_local import generate_soil_type_local +from computing.misc.naturaldepression import generate_natural_depression_data +from computing.misc.distancetonearestdrainage import ( + generate_distance_to_nearest_drainage_line, +) +from computing.misc.catchment_area import generate_catchment_area_singleflow +from computing.misc.slope_percentage import generate_slope_percentage_data +from computing.misc.lcw_conflict import generate_lcw_conflict_data +from computing.misc.agroecological_space import generate_agroecological_data +from computing.misc.factory_csr import generate_factory_csr_data +from computing.misc.green_credit import generate_green_credit_data +from computing.misc.mining_data import generate_mining_data +from computing.plantation.site_suitability import site_suitability +from computing.mws.mws_connectivity import generate_mws_connectivity_data +from computing.misc.ndvi_time_series import ndvi_timeseries +from computing.zoi_layers.zoi import generate_zoi +from computing.mws.mws_centroid import generate_mws_centroid_data +from computing.change_detection.change_detection_local import ( + get_change_detection as get_change_detection_local, +) +from computing.change_detection.change_detection_vector_local import ( + vectorise_change_detection as vectorise_change_detection_local, +) +from computing.cropping_intensity.cropping_intesity_local import ( + generate_cropping_intensity as generate_cropping_intensity_local, +) +from computing.lulc.lulc_v3_local import clip_lulc_v3 as clip_lulc_v3_local +from computing.lulc.lulc_vector_local import vectorise_lulc as vectorise_lulc_local +from computing.lulc_X_terrain.lulc_on_plain_cluster_local import ( + lulc_on_plain_cluster_local, +) +from computing.lulc_X_terrain.lulc_on_slope_cluster_local import ( + lulc_on_slope_cluster_local, +) +from computing.misc.agroecological_space_local_compute import ( + generate_agroecological_data_local, +) +from computing.misc.aquifer_vector_local import ( + generate_aquifer_vector as generate_aquifer_vector_local, +) +from computing.misc.catchment_area_local_compute import ( + generate_catchment_area_singleflow_local, +) +from computing.misc.distancetonearestdrainage_local_compute import ( + generate_distance_to_nearest_drainage_line_local, +) +from computing.misc.drainage_lines_local_compute import ( + clip_drainage_lines as clip_drainage_lines_local, +) +from computing.misc.facilities.pipeline import generate_facilities_proximity_task +from computing.misc.antyodaya.pipeline import generate_antyodaya_layer_task +from computing.misc.canal_local_compute import canal_vector as canal_vector_local +from computing.misc.digital_elevation_model_local import ( + generate_febdem_raster_vector_clip, +) +from computing.misc.drainage_density_local_compute import drainage_density +from computing.misc.livestocks.pipeline import generate_livestocks_layer_task +from computing.misc.river_local_compute import river_vector as river_vector_local +from computing.misc.factory_csr_local_compute import generate_factory_csr_data_local +from computing.misc.green_credit_local_compute import generate_green_credit_data_local +from computing.misc.lcw_conflict_local_compute import generate_lcw_conflict_data_local +from computing.misc.mining_data_local_compute import generate_mining_data_local +from computing.misc.naturaldepression_local_compute import ( + generate_natural_depression_data_local, +) +from computing.misc.nrega_local_compute import generate_nrega_data_local +from computing.misc.restoration_opportunity_local_compute import ( + generate_restoration_opportunity_local, +) +from computing.misc.slope_percentage_local_compute import ( + generate_slope_percentage_data_local, +) +from computing.misc.soge_vector_local_compute import generate_soge_vector_local +from computing.mws.mws_centroid_local_compute import generate_mws_centroid_data_local +from computing.mws.mws_connectivity_local_compute import mws_connectivity_vector +from computing.surface_water_bodies.swb_local import ( + generate_swb_layer as generate_swb_layer_local, +) +from computing.terrain_descriptor.terrain_clusters_local import ( + generate_terrain_clusters as generate_terrain_clusters_local, +) +from computing.terrain_descriptor.terrain_compute_all_local import ( + generate_terrain_compute_all, +) +from computing.terrain_descriptor.terrain_raster_fabdem_local import ( + generate_terrain_raster_clip as terrain_raster_local, +) +from stats_generator.utils import generate_stats_excel_file +from utilities.gee_utils import valid_gee_text +import os +import logging +from nrm_app.celery import app +from computing.models import Layer +import json + +logger = logging.getLogger(__name__) + +VALID_COMPUTE_TYPES = {"gee", "local"} + +CONFIG_DIR = os.path.dirname(__file__) +EXTERNAL_CONFIG_DIR = os.path.join("data", "layers", "layer_dependency") + +MAP_CONFIG_FILES = { + "gee": "layer_map.json", + "local": "local_layer_map.json", +} + +END_YEAR_RULE_FILES = { + "gee": "end_year_rules.json", + "local": "local_end_year_rules.json", +} + + +GEE_TASK_REGISTRY = { + "generate_tehsil_shape_file_data": generate_tehsil_shape_file_data, + "clip_nrega_district_block": clip_nrega_district_block, + "generate_nrega_data": clip_nrega_district_block, + "generate_nrega_layer": clip_nrega_district_block, + "mws_layer": mws_layer, + "generate_mws_layer": mws_layer, + "generate_hydrology": generate_hydrology, + "clip_lulc_v3": clip_lulc_v3, + "lulc_v3": clip_lulc_v3, + "vectorise_lulc": vectorise_lulc, + "lulc_vector": vectorise_lulc, + "generate_cropping_intensity": generate_cropping_intensity, + "generate_ci_layer": generate_cropping_intensity, + "generate_swb_layer": generate_swb_layer, + "generate_swb": generate_swb_layer, + "calculate_drought": calculate_drought, + "generate_drought_layer": calculate_drought, + "drought_causality": drought_causality, + "mws_drought_causality": drought_causality, + "create_crop_grids": create_crop_grids, + "crop_grid": create_crop_grids, + "get_change_detection": get_change_detection, + "change_detection": get_change_detection, + "vectorise_change_detection": vectorise_change_detection, + "change_detection_vector": vectorise_change_detection, + "generate_restoration_opportunity": generate_restoration_opportunity, + "restoration_opportunity": generate_restoration_opportunity, + "generate_aquifer_vector": generate_aquifer_vector, + "aquifer_vector": generate_aquifer_vector, + "terrain_raster": terrain_raster, + "generate_terrain_raster": terrain_raster, + "generate_terrain_clusters": generate_terrain_clusters, + "generate_terrain_descriptor": generate_terrain_clusters, + "lulc_on_plain_cluster": lulc_on_plain_cluster, + "terrain_lulc_plain_cluster": lulc_on_plain_cluster, + "lulc_on_slope_cluster": lulc_on_slope_cluster, + "terrain_lulc_slope_cluster": lulc_on_slope_cluster, + "generate_soge_vector": generate_soge_vector, + "soge_vector": generate_soge_vector, + "generate_stream_order": generate_stream_order, + "stream_order": generate_stream_order, + "clip_drainage_lines": clip_drainage_lines, + "generate_drainage_layer": clip_drainage_lines, + "generate_clart_layer": generate_clart_layer, + "generate_clart": generate_clart_layer, + "tree_health_ch_raster": tree_health_ch_raster, + "tree_health_ch_vector": tree_health_ch_vector, + "tree_health_ccd_raster": tree_health_ccd_raster, + "tree_health_ccd_vector": tree_health_ccd_vector, + "tree_health_overall_change_raster": tree_health_overall_change_raster, + "tree_health_overall_change_vector": tree_health_overall_change_vector, + "generate_natural_depression_data": generate_natural_depression_data, + "generate_natural_depression": generate_natural_depression_data, + "generate_distance_to_nearest_drainage_line": generate_distance_to_nearest_drainage_line, + "generate_distance_nearest_DL": generate_distance_to_nearest_drainage_line, + "generate_catchment_area_singleflow": generate_catchment_area_singleflow, + "generate_slope_percentage_data": generate_slope_percentage_data, + "generate_slope_percentage": generate_slope_percentage_data, + "generate_lcw_conflict_data": generate_lcw_conflict_data, + "generate_lcw": generate_lcw_conflict_data, + "generate_agroecological_data": generate_agroecological_data, + "generate_agroecological": generate_agroecological_data, + "generate_factory_csr_data": generate_factory_csr_data, + "generate_factory_csr": generate_factory_csr_data, + "generate_green_credit_data": generate_green_credit_data, + "generate_green_credit": generate_green_credit_data, + "generate_mining_data": generate_mining_data, + "generate_mining": generate_mining_data, + "site_suitability": site_suitability, + "plantation_site_suitability": site_suitability, + "generate_mws_connectivity_data": generate_mws_connectivity_data, + "generate_mws_connectivity": generate_mws_connectivity_data, + "ndvi_timeseries": ndvi_timeseries, + "generate_ndvi_timeseries": ndvi_timeseries, + "generate_zoi": generate_zoi, + "generate_zoi_data": generate_zoi, + "generate_mws_centroid_data": generate_mws_centroid_data, + "generate_mws_centroid": generate_mws_centroid_data, +} + +LOCAL_TASK_REGISTRY = { + "clip_nrega_district_block": generate_nrega_data_local, + "generate_nrega_data": generate_nrega_data_local, + "generate_nrega_layer": generate_nrega_data_local, + "clip_lulc_v3": clip_lulc_v3_local, + "lulc_v3": clip_lulc_v3_local, + "vectorise_lulc": vectorise_lulc_local, + "lulc_vector": vectorise_lulc_local, + "generate_cropping_intensity": generate_cropping_intensity_local, + "generate_ci_layer": generate_cropping_intensity_local, + "generate_swb_layer": generate_swb_layer_local, + "generate_swb": generate_swb_layer_local, + "get_change_detection": get_change_detection_local, + "change_detection": get_change_detection_local, + "vectorise_change_detection": vectorise_change_detection_local, + "change_detection_vector": vectorise_change_detection_local, + "generate_aquifer_vector": generate_aquifer_vector_local, + "aquifer_vector": generate_aquifer_vector_local, + "terrain_raster": terrain_raster_local, + "generate_terrain_raster": terrain_raster_local, + "generate_terrain_clusters": generate_terrain_clusters_local, + "generate_terrain_descriptor": generate_terrain_clusters_local, + "generate_terrain_compute_all": generate_terrain_compute_all, + "lulc_on_plain_cluster": lulc_on_plain_cluster_local, + "terrain_lulc_plain_cluster": lulc_on_plain_cluster_local, + "lulc_on_slope_cluster": lulc_on_slope_cluster_local, + "terrain_lulc_slope_cluster": lulc_on_slope_cluster_local, + "generate_soge_vector": generate_soge_vector_local, + "soge_vector": generate_soge_vector_local, + "clip_drainage_lines": clip_drainage_lines_local, + "generate_drainage_layer": clip_drainage_lines_local, + "generate_natural_depression_data": generate_natural_depression_data_local, + "generate_natural_depression": generate_natural_depression_data_local, + "generate_distance_to_nearest_drainage_line": generate_distance_to_nearest_drainage_line_local, + "generate_distance_nearest_DL": generate_distance_to_nearest_drainage_line_local, + "generate_catchment_area_singleflow": generate_catchment_area_singleflow_local, + "generate_slope_percentage_data": generate_slope_percentage_data_local, + "generate_slope_percentage": generate_slope_percentage_data_local, + "generate_lcw_conflict_data": generate_lcw_conflict_data_local, + "generate_lcw": generate_lcw_conflict_data_local, + "generate_agroecological_data": generate_agroecological_data_local, + "generate_agroecological": generate_agroecological_data_local, + "generate_factory_csr_data": generate_factory_csr_data_local, + "generate_factory_csr": generate_factory_csr_data_local, + "generate_green_credit_data": generate_green_credit_data_local, + "generate_green_credit": generate_green_credit_data_local, + "generate_mining_data": generate_mining_data_local, + "generate_mining": generate_mining_data_local, + "generate_restoration_opportunity": generate_restoration_opportunity_local, + "restoration_opportunity": generate_restoration_opportunity_local, + "generate_mws_connectivity_data": mws_connectivity_vector, + "generate_mws_connectivity": mws_connectivity_vector, + "generate_facilities_proximity": generate_facilities_proximity_task, + "generate_livestocks": generate_livestocks_layer_task, + "generate_antyodaya": generate_antyodaya_layer_task, + "generate_density_vector": drainage_density, + "generate_river_data": river_vector_local, + "generate_canal_vector": canal_vector_local, + "generate_dem_raster_vector": generate_febdem_raster_vector_clip, + "generate_mws_centroid_data": generate_mws_centroid_data_local, + "generate_mws_centroid": generate_mws_centroid_data_local, + "tree_health_ch_raster": tree_health_ch_raster_local, + "tree_health_ch_vector": tree_health_ch_vector_local, + "tree_health_ccd_raster": tree_health_ccd_raster_local, + "tree_health_ccd_vector": tree_health_ccd_vector_local, + "tree_health_overall_change_raster": tree_health_overall_change_raster_local, + "tree_health_overall_change_vector": tree_health_overall_change_vector_local, + "soil_health": soil_health_local, + "generate_soil_type": generate_soil_type_local, + "soil_type": generate_soil_type_local, +} + +TASK_REGISTRIES = { + "gee": GEE_TASK_REGISTRY, + "local": LOCAL_TASK_REGISTRY, +} + + +@app.task(bind=True) +@with_tehsil_watershed +def layer_generate_map( + self, + state, + district, + block, + map_order, + gee_account_id, + start_year=None, + end_year=None, + compute="gee", +): + """ + This function take state, district,block and map_order(map to trigger, it can be map_1, map_2_1, map_2_2, map_3, map_4). One map trigger more numbers of pipeline. + + Sibling nodes (nodes that don't depend on each other) keep running even if one of + them fails; only nodes that declare a dependency on a failed node are skipped. + """ + compute = normalize_compute(compute) + log_ctx = f"state={state}, district={district}, block={block}, map={map_order}, compute={compute}" + status = {} + + # checking:- is mws layer generated? + try: + if compute == "gee" and map_order in ["map_2_1", "map_2_2", "map_3", "map_4"]: + layer = ( + Layer.objects.filter( + layer_name=f"mws_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}" + ) + .order_by("-layer_version") + .first() + ) + if not layer: + logger.error(f"MWS layer missing, cannot proceed ({log_ctx})") + return f"check mws layer for {district}_{block}" + except Exception as e: + logger.exception(f"Exception while checking mws layer ({log_ctx})") + return f"exception occur while checking mws for {district}_{block} as: {e}" + + global_args = {} + if start_year: + global_args["start_year"] = start_year + if end_year: + global_args["end_year"] = end_year + + # Load JSON configuration + map_config = load_map_config(map_order, compute=compute) + + if not map_config: + logger.error(f"Map configuration not found ({log_ctx})") + return f"Map configuration not found for {map_order} using compute={compute}" + + validation_errors = validate_map_config(map_config, compute=compute) + if validation_errors: + logger.error( + f"Invalid map configuration ({log_ctx}): {'; '.join(validation_errors)}" + ) + return ( + f"Invalid {compute} map configuration for {map_order}: " + f"{'; '.join(validation_errors)}" + ) + + logger.info(f"Starting layer generation ({log_ctx})") + task_registry = TASK_REGISTRIES[compute] + for func in map_config: + run_node_tree( + node=func, + task_registry=task_registry, + compute=compute, + state=state, + district=district, + block=block, + global_args=global_args, + gee_account_id=gee_account_id, + status=status, + ) + + failed_nodes = sorted(name for name, ok in status.items() if not ok) + succeeded_nodes = sorted(name for name, ok in status.items() if ok) + if failed_nodes: + logger.error( + f"Layer generation completed with failures ({log_ctx}): " + f"failed={failed_nodes}, succeeded={succeeded_nodes}" + ) + else: + logger.info( + f"Layer generation completed successfully ({log_ctx}): " + f"succeeded={succeeded_nodes}" + ) + + return f"{status = }" + + +def normalize_compute(compute): + compute = str(compute or "gee").strip().lower() + if compute not in VALID_COMPUTE_TYPES: + raise ValueError("compute must be either 'gee' or 'local'") + return compute + + +def _candidate_config_paths(filename): + return [ + os.path.join(EXTERNAL_CONFIG_DIR, filename), + os.path.join(CONFIG_DIR, filename), + ] + + +def _load_json_config(filename, default=None): + for config_path in _candidate_config_paths(filename): + if os.path.exists(config_path): + with open(config_path, "r") as f: + return json.load(f) + if default is not None: + return default + raise FileNotFoundError( + f"Layer dependency config not found. Checked: " + f"{_candidate_config_paths(filename)}" + ) + + +def load_map_config(map_order, compute="gee"): + """ + Load map configuration from JSON file based on map_order. + """ + compute = normalize_compute(compute) + all_configs = _load_json_config(MAP_CONFIG_FILES[compute], default={}) + return all_configs.get(map_order, []) + + +def load_end_year_rules(compute="gee"): + """ + Load end year rules from JSON. + """ + compute = normalize_compute(compute) + return _load_json_config(END_YEAR_RULE_FILES[compute], default={}) + + +def flatten_map_nodes(map_config): + nodes = [] + for node in map_config: + nodes.append(node) + nodes.extend(flatten_map_nodes(node.get("children", []))) + return nodes + + +def validate_layer_map_request(map_order, compute="gee"): + compute = normalize_compute(compute) + map_config = load_map_config(map_order, compute=compute) + if not map_config: + return [f"Map configuration not found for {map_order} using compute={compute}"] + return validate_map_config(map_config, compute=compute) + + +def validate_map_config(map_config, compute="gee"): + compute = normalize_compute(compute) + task_registry = TASK_REGISTRIES[compute] + node_names = {node.get("name") for node in flatten_map_nodes(map_config)} + validator_names = set(DependencyValidator.names_for_compute(compute)) + + unsupported_nodes = sorted( + node_name for node_name in node_names if node_name not in task_registry + ) + unsupported_deps = sorted( + dep + for node in flatten_map_nodes(map_config) + for dep in node.get("depends_on", []) + if dep not in node_names and dep not in validator_names + ) + + errors = [] + if unsupported_nodes: + errors.append(f"unsupported {compute} nodes: {', '.join(unsupported_nodes)}") + if unsupported_deps: + errors.append( + f"unsupported {compute} dependencies: {', '.join(unsupported_deps)}" + ) + return errors + + +# check the dependency layer is available or not +class DependencyValidator: + GEE_VALIDATORS = {} + LOCAL_VALIDATORS = {} + + @staticmethod + def _local_layer_exists(layer_name=None, dataset_name=None): + qs = Layer.objects.filter(misc__is_generated_locally=True) + if layer_name: + qs = qs.filter(layer_name=layer_name) + if dataset_name: + qs = qs.filter(dataset__name=dataset_name) + return qs.exists() + + @classmethod + def get_checker(cls, dep, compute): + compute = normalize_compute(compute) + if compute == "local": + return cls.LOCAL_VALIDATORS.get(dep) + return cls.GEE_VALIDATORS.get(dep) + + @classmethod + def names_for_compute(cls, compute): + compute = normalize_compute(compute) + if compute == "local": + return set(cls.LOCAL_VALIDATORS) + return set(cls.GEE_VALIDATORS) + + @staticmethod + def clip_lulc_v3(district, block): + return ( + Layer.objects.filter( + layer_name__icontains=f"{valid_gee_text(district)}_{valid_gee_text(block)}_level_" + ).count() + == 24 + ) + + @staticmethod + def terrain_raster(district, block): + return Layer.objects.filter( + layer_name=f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_terrain_raster" + ).exists() + + @staticmethod + def generate_catchment_area_singleflow(district, block): + return Layer.objects.filter( + layer_name=f"catchment_area_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_raster" + ).exists() + + @staticmethod + def generate_stream_order(district, block): + return Layer.objects.filter( + layer_name=f"stream_order_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_vector" + ).exists() + + @staticmethod + def clip_drainage_lines(district, block): + return Layer.objects.filter( + layer_name=f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}", + dataset__name="Drainage", + ).exists() + + @staticmethod + def generate_cropping_intensity(district, block): + return Layer.objects.filter( + layer_name=f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_intensity" + ).exists() + + @staticmethod + def generate_swb_layer(district, block): + return Layer.objects.filter( + layer_name=f"surface_waterbodies_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}" + ).exists() + + @staticmethod + def generate_tehsil_shape_file_data(district, block): + return Layer.objects.filter( + layer_name=f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}", + dataset__name="Admin Boundary", + ).exists() + + @staticmethod + def local_layer_generated(district, block): + return ( + Layer.objects.filter( + layer_name__icontains=valid_gee_text(district.lower()), + misc__is_generated_locally=True, + ) + .filter(layer_name__icontains=valid_gee_text(block.lower())) + .exists() + ) + + +DependencyValidator.GEE_VALIDATORS = { + name: getattr(DependencyValidator, name) + for name in [ + "clip_lulc_v3", + "terrain_raster", + "generate_catchment_area_singleflow", + "generate_stream_order", + "clip_drainage_lines", + "generate_cropping_intensity", + "generate_swb_layer", + "generate_tehsil_shape_file_data", + ] +} + +DependencyValidator.LOCAL_VALIDATORS = { + # Local ordered maps mostly depend on nodes generated earlier in the same run. + # This fallback is for externally pre-generated local dependencies. + "local_layer_generated": DependencyValidator.local_layer_generated, +} + + +def run_node_tree( + node, + task_registry, + compute, + state, + district, + block, + global_args, + gee_account_id, + status, +): + node_func_name = node["name"] + node_func_obj = task_registry[node_func_name] + args = get_args( + iterator_name=node, + global_args=global_args, + gee_account_id=gee_account_id, + compute=compute, + ) + deps = node.get("depends_on", []) + run_layer_with_dependency( + deps=deps, + node_func_name=node_func_name, + node_func_obj=node_func_obj, + compute=compute, + state=state, + district=district, + block=block, + args=args, + status=status, + ) + + if not status.get(node_func_name, False): + # Only nodes depending on this one are affected; siblings keep running. + return + + for child in node.get("children", []): + run_node_tree( + node=child, + task_registry=task_registry, + compute=compute, + state=state, + district=district, + block=block, + global_args=global_args, + gee_account_id=gee_account_id, + status=status, + ) + + +def run_layer_with_dependency( + deps, node_func_name, node_func_obj, compute, state, district, block, args, status +): + """ + This function checks dependency of layer if it is generated or not and call the pipeline functions and maintain status of each function, + """ + log_ctx = ( + f"node={node_func_name}, state={state}, district={district}, block={block}" + ) + for dep in deps: + if status.get(dep, False): + continue + checker = DependencyValidator.get_checker(dep, compute) + status[dep] = checker(district, block) if checker else False + if not status[dep]: + logger.warning( + f"Skipping {node_func_name} because dependency {dep} failed or was not executed ({log_ctx})" + ) + status[node_func_name] = False + break + else: + try: + end_year_rules = load_end_year_rules(compute=compute) + if node_func_name in end_year_rules: + args["end_year"] = end_year_rules[node_func_name] + if node_func_name == "site_suitability": + args["project_id"] = None + logger.info( + f"Running {node_func_name} with args={args}, depends_on={deps} ({log_ctx})" + ) + if node_func_name == "generate_stats_excel_file": + result = node_func_obj(state, district, block) + else: + result = ( + node_func_obj(state=state, district=district, block=block, **args) + if args + else node_func_obj(state, district, block) + ) + if result: + status[node_func_name] = True + logger.info(f"Completed {node_func_name} ({log_ctx}): result={result}") + else: + status[node_func_name] = False + logger.warning( + f"{node_func_name} returned a falsy result, treating as failed ({log_ctx}): result={result}" + ) + except Exception: + status[node_func_name] = False + logger.exception(f"{node_func_name} raised an error ({log_ctx})") + + +def get_args(iterator_name, global_args, gee_account_id, compute="gee"): + """ + This function merge the global agrs and local args(define in json maps) return combination of both. + """ + arg = iterator_name.get("args", {}) + args = dict(arg) + if normalize_compute(compute) == "gee" or iterator_name.get( + "pass_gee_account_id", False + ): + args["gee_account_id"] = gee_account_id + if iterator_name.get("use_global_args", False): + args = { + **global_args, + **args, + } + return args diff --git a/computing/layer_dependency/local_end_year_rules.json b/computing/layer_dependency/local_end_year_rules.json new file mode 100644 index 00000000..13c0c615 --- /dev/null +++ b/computing/layer_dependency/local_end_year_rules.json @@ -0,0 +1,7 @@ +{ + "tree_health_ch_raster": 2023, + "tree_health_ch_vector": 2023, + "tree_health_ccd_raster": 2023, + "tree_health_ccd_vector": 2023, + "tree_health_overall_change_raster": 2023 +} diff --git a/computing/layer_dependency/local_layer_map.json b/computing/layer_dependency/local_layer_map.json new file mode 100644 index 00000000..749e236d --- /dev/null +++ b/computing/layer_dependency/local_layer_map.json @@ -0,0 +1,158 @@ +{ + "dynamic_layers": [ + { + "name": "generate_nrega_layer" + }, + { + "name": "lulc_v3", + "use_global_args": true + }, + { + "name": "lulc_vector", + "depends_on": ["lulc_v3"], + "use_global_args": true + }, + { + "name": "change_detection", + "use_global_args": true + }, + { + "name": "change_detection_vector", + "depends_on": ["change_detection"], + "use_global_args": true + }, + { + "name": "generate_terrain_raster" + }, + { + "name": "generate_terrain_descriptor", + "depends_on": ["generate_terrain_raster"] + }, + { + "name": "terrain_lulc_plain_cluster", + "depends_on": ["generate_terrain_raster", "lulc_v3"], + "use_global_args": true + }, + { + "name": "terrain_lulc_slope_cluster", + "depends_on": ["generate_terrain_raster", "lulc_v3"], + "use_global_args": true + }, + { + "name": "generate_ci_layer", + "use_global_args": true + }, + { + "name": "generate_swb", + "use_global_args": true, + "pass_gee_account_id": true + }, + { + "name": "tree_health_ch_raster", + "depends_on": ["lulc_v3"], + "use_global_args": true, + "children": [ + { + "name": "tree_health_ch_vector", + "use_global_args": true + } + ] + }, + { + "name": "tree_health_ccd_raster", + "depends_on": ["lulc_v3"], + "use_global_args": true, + "children": [ + { + "name": "tree_health_ccd_vector", + "use_global_args": true + } + ] + }, + { + "name": "tree_health_overall_change_raster", + "depends_on": ["change_detection"], + "use_global_args": true, + "children": [ + { + "name": "tree_health_overall_change_vector", + "use_global_args": false + } + ] + }, + { + "name": "soil_health" + } + ], + "static_layers": [ + { + "name": "aquifer_vector" + }, + { + "name": "generate_livestocks" + }, + { + "name": "generate_antyodaya" + }, + { + "name": "generate_density_vector" + }, + { + "name": "generate_river_data" + }, + { + "name": "generate_canal_vector" + }, + { + "name": "generate_dem_raster_vector" + }, + { + "name": "generate_facilities_proximity" + }, + { + "name": "generate_drainage_layer" + }, + { + "name": "restoration_opportunity" + }, + { + "name": "soge_vector" + }, + { + "name": "generate_lcw" + }, + { + "name": "generate_agroecological" + }, + { + "name": "generate_factory_csr" + }, + { + "name": "generate_green_credit" + }, + { + "name": "generate_mining" + }, + { + "name": "generate_natural_depression" + }, + { + "name": "generate_distance_nearest_DL" + }, + { + "name": "generate_catchment_area_singleflow" + }, + { + "name": "generate_slope_percentage" + }, + { + "name": "generate_mws_connectivity_data" + }, + { + "name": "generate_mws_centroid" + }, + { + "name": "soil_type" + } + ] +} diff --git a/computing/local_compute_helper.py b/computing/local_compute_helper.py new file mode 100644 index 00000000..4c3d6bb2 --- /dev/null +++ b/computing/local_compute_helper.py @@ -0,0 +1,1060 @@ +import os +from contextlib import ExitStack +from pathlib import Path + +import geopandas as gpd +import numpy as np +import pandas as pd +import rasterio +from rasterio.mask import mask +from shapely.geometry import mapping +from utilities.gee_utils import valid_gee_text + +from computing.utils import convert_to_zip +import logging + + +from computing.config_loader import ( + AEZ_VECTOR_PATH, + LULC_BASE_DIR, + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + PROJECT_ROOT, + TERRAIN_RASTER_PATH, +) +from computing.base_layer_setup import ensure_tehsil_watershed +from utilities.download_gpkg_from_geoserver import generate_gpkg + +PRECOMPUTED_PANCHAYAT_DIR = PROJECT_ROOT / "data/base_layers/village_boundaries" + +PRECOMPUTED_ROI_EXTENSIONS = (".gpkg", ".geojson") +VALID_COMPUTE_TYPES = {"gee", "local"} +MIN_WATERSHED_AREA_HA = 400.0 +LULC_CLASSES = np.arange(1, 13, dtype=np.int16) + +TERRAIN_CLUSTER_CENTROIDS = np.array( + [ + [0.36255426, 0.21039965, 0.12161905, 0.17393119, 0.13149585], + [0.09171062, 0.84299211, 0.035222, 0.02172654, 0.00834873], + [0.08497599, 0.01051893, 0.23763531, 0.37992855, 0.28694122], + [0.22301813, 0.5611825, 0.08511123, 0.07314189, 0.05754624], + ], + dtype=np.float64, +) + +PLAIN_CLASSES = {5} +VALLEY_CLASSES = {1, 2, 4, 9} +HILL_SLOPES_CLASSES = {8} +RIDGE_CLASSES = {3, 7, 10, 11} +SLOPY_CLASSES = {6} + + +def _slug(value, fallback): + return valid_gee_text(str(value).strip().lower()) or fallback + + +def ensure_file_exists(path, label): + path = Path(path) + if not path.exists(): + raise FileNotFoundError(f"{label} not found: {path}") + + +def validate_geometry(gdf): + gdf = gdf[gdf.geometry.notna()].copy() + if gdf.empty: + return gdf + invalid = ~gdf.is_valid + if invalid.any(): + gdf.loc[invalid, "geometry"] = gdf.loc[invalid, "geometry"].buffer(0) + return gdf[~gdf.geometry.is_empty].copy() + + +def read_validated_vector_file(path, empty_message): + gdf = validate_geometry(gpd.read_file(path)) + if gdf.empty: + raise ValueError(empty_message) + return gdf + + +def resolve_precomputed_vector_file( + state, + district, + block, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + extensions=PRECOMPUTED_ROI_EXTENSIONS, + missing_file_label="Precomputed vector file", +): + roi_dir = Path(precomputed_roi_dir or PRECOMPUTED_TEHSIL_WATERSHED_DIR) + state_slug = _slug(state, "unknown_state") + district_slug = _slug(district, "unknown_district") + block_slug = _slug(block, "unknown_tehsil") + + expected_paths = [ + roi_dir / state_slug / district_slug / f"{block_slug}{ext}" + for ext in extensions + ] + for path in expected_paths: + if path.exists(): + return path + + raise FileNotFoundError( + f"{missing_file_label} not found. " + f"state={state}, district={district}, block={block}. " + f"Expected one of: {[str(path) for path in expected_paths]}" + ) + + +def resolve_precomputed_panchayat_vector_file( + state, + district, + block, + precomputed_roi_dir=PRECOMPUTED_PANCHAYAT_DIR, + extensions=PRECOMPUTED_ROI_EXTENSIONS, + missing_file_label="Precomputed vector file", +): + roi_dir = Path(precomputed_roi_dir or PRECOMPUTED_PANCHAYAT_DIR) + state_slug = _slug(state, "unknown_state") + district_slug = _slug(district, "unknown_district") + block_slug = _slug(block, "unknown_tehsil") + + expected_paths = [ + roi_dir / state_slug / district_slug / f"{block_slug}{ext}" + for ext in extensions + ] + for path in expected_paths: + if path.exists(): + return path + + raise FileNotFoundError( + f"{missing_file_label} not found. " + f"state={state}, district={district}, block={block}. " + f"Expected one of: {[str(path) for path in expected_paths]}" + ) + + +def load_precomputed_watersheds( + state, + district, + block, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, +): + try: + watershed_path = resolve_precomputed_vector_file( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + missing_file_label="Precomputed watershed boundary file", + ) + + except FileNotFoundError: + print(f"Precomputed watershed not found for " f"{state}/{district}/{block}") + ensure_tehsil_watershed( + state=state, + district=district, + tehsil=block, + ) + watershed_path = resolve_precomputed_vector_file( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + missing_file_label="Generated watershed boundary file not found", + ) + + watersheds_gdf = read_validated_vector_file( + watershed_path, + f"Precomputed watershed file has no valid geometries: {watershed_path}", + ) + print(f"Loaded watershed boundaries: {watershed_path}") + return watersheds_gdf, str(watershed_path) + + +def load_precomputed_panchayat( + state, + district, + block, + precomputed_roi_dir=PRECOMPUTED_PANCHAYAT_DIR, +): + try: + panchayat_path = resolve_precomputed_panchayat_vector_file( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + missing_file_label="Precomputed panchayat boundary file", + ) + + except FileNotFoundError: + print(f"Precomputed panchayat not found for " f"{state}/{district}/{block}") + generate_gpkg( + state=state, + district=district, + block=block, + workspace="panchayat_boundaries", + ) + + panchayat_path = resolve_precomputed_panchayat_vector_file( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + missing_file_label="Generated panchayat boundary file not found", + ) + + panchayat_gdf = read_validated_vector_file( + panchayat_path, + f"Precomputed panchayat file has no valid geometries: {panchayat_path}", + ) + + print(f"Loaded panchayat boundaries: {panchayat_path}") + return panchayat_gdf, str(panchayat_path) + + +def load_precomputed_roi( + state, + district, + block, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, +): + try: + roi_path = resolve_precomputed_vector_file( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + missing_file_label="Precomputed tehsil watershed file", + ) + except FileNotFoundError: + print(f"Precomputed ROI not found for {state}/{district}/{block}. Downloading...") + ensure_tehsil_watershed( + state=state, + district=district, + tehsil=block, + ) + roi_path = resolve_precomputed_vector_file( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + missing_file_label="Generated tehsil watershed file", + ) + + roi_gdf = read_validated_vector_file( + roi_path, + f"Precomputed ROI file has no valid geometries: {roi_path}", + ) + print(f"Loaded precomputed ROI file: {roi_path}") + return roi_gdf + + +def _build_output_dir( + output_base_dir, + state=None, + district=None, + block=None, + custom_subdir="custom", + block_fallback="unknown_tehsil", +): + output_base_dir = Path(output_base_dir) + if state and district and block: + output_dir = ( + output_base_dir + / _slug(state, "unknown_state") + / _slug(district, "unknown_district") + / _slug(block, block_fallback) + ) + else: + output_dir = output_base_dir / custom_subdir + output_dir.mkdir(parents=True, exist_ok=True) + return output_dir + + +def build_output_vector_path( + layer_name, + state, + district, + block, + output_base_dir, + custom_subdir="custom", + block_fallback="unknown_block", +): + output_dir = _build_output_dir( + output_base_dir=output_base_dir, + state=state, + district=district, + block=block, + custom_subdir=custom_subdir, + block_fallback=block_fallback, + ) + return output_dir / f"{layer_name}.gpkg" + + +def build_output_raster_path( + layer_name, + output_base_dir, + state=None, + district=None, + block=None, + custom_subdir="custom", + block_fallback="unknown_tehsil", +): + output_dir = _build_output_dir( + output_base_dir=output_base_dir, + state=state, + district=district, + block=block, + custom_subdir=custom_subdir, + block_fallback=block_fallback, + ) + return output_dir / f"{layer_name}.tif" + + +def write_vector_output(gdf, output_path, layer_name): + gdf.to_file(output_path, driver="GPKG", layer=layer_name) + return str(output_path) + + +def clip_raster_with_roi(roi_gdf, raster_path, output_path, raster_label="Raster"): + ensure_file_exists(raster_path, raster_label) + + with rasterio.open(raster_path) as src: + clip_gdf = roi_gdf + if src.crs and clip_gdf.crs and clip_gdf.crs != src.crs: + clip_gdf = clip_gdf.to_crs(src.crs) + + clip_gdf = validate_geometry(clip_gdf) + shapes = [ + mapping(geom) + for geom in clip_gdf.geometry + if geom is not None and not geom.is_empty + ] + if not shapes: + raise ValueError("No valid ROI geometry available for raster clipping.") + + clipped_data, clipped_transform = mask(src, shapes=shapes, crop=True) + clipped_meta = src.meta.copy() + clipped_meta.update( + { + "driver": "GTiff", + "height": clipped_data.shape[1], + "width": clipped_data.shape[2], + "transform": clipped_transform, + "compress": "lzw", + } + ) + + with rasterio.open(output_path, "w", **clipped_meta) as dst: + dst.write(clipped_data) + + return str(output_path) + + +RASTER_DECLARED_SRS = "EPSG:4326" +VECTOR_DECLARED_SRS = "EPSG:4326" + + +def _push_raster_to_geoserver_instance(geo, file_path, layer_name, workspace, style_name): + + _log = logging.getLogger(__name__) + + geo.ensure_workspace(workspace) + geo.delete_raster_store(workspace=workspace, store=layer_name) + upload_response = geo.create_coveragestore( + path=file_path, + workspace=workspace, + layer_name=layer_name, + ) + try: + geo.configure_coverage( + workspace=workspace, + store=layer_name, + coverage=layer_name, + srs=RASTER_DECLARED_SRS, + projection_policy="REPROJECT_TO_DECLARED", + enabled=True, + ) + except Exception as e: + _log.warning( + "Failed to force SRS on coverage %s:%s: %s", + workspace, + layer_name, + e, + ) + style_response = None + if style_name: + style_response = geo.publish_style( + layer_name=layer_name, + style_name=style_name, + workspace=workspace, + ) + return upload_response, style_response + + +def _verify_raster_layer(geo, workspace, layer_name): + """Return True iff a layer (not just the store) is registered on the server.""" + from utilities.geoserver_utils import GeoserverException + + try: + geo.get_layer(layer_name=layer_name, workspace=workspace) + return True + except GeoserverException: + return False + except Exception: + return False + + +def _push_vector_to_geoserver_instance(geo, path, layer_name, workspace, file_type="gpkg"): + _log = logging.getLogger(__name__) + + geo.ensure_workspace(workspace) + try: + geo.delete_vector_store(workspace=workspace, store=layer_name) + except Exception: + pass + + zip_path = convert_to_zip(path, file_type) + response = geo.create_shp_datastore( + path=zip_path, + store_name=layer_name, + workspace=workspace, + file_extension=file_type, + ) + try: + geo.configure_featuretype( + workspace=workspace, + store=layer_name, + featuretype=layer_name, + srs=VECTOR_DECLARED_SRS, + projection_policy="FORCE_DECLARED", + ) + except Exception as e: + _log.warning( + "Failed to force SRS on featuretype %s:%s: %s", + workspace, + layer_name, + e, + ) + return response + + +def push_local_vector_to_geoserver(path, layer_name, workspace, file_type="gpkg"): + import logging + from django.conf import settings + from utilities.geoserver_utils import Geoserver + + _log = logging.getLogger(__name__) + + local_geo = Geoserver() + local_response = _push_vector_to_geoserver_instance( + local_geo, path, layer_name, workspace, file_type + ) + local_ok = isinstance(local_response, dict) and local_response.get("status_code") in ( + 200, + 201, + ) + _log.info( + "Pushed vector %s to local GeoServer (ok=%s).", layer_name, local_ok + ) + + prod_url = getattr(settings, "PROD_GEOSERVER_URL", "") + prod_response = None + prod_ok = None + if prod_url: + try: + prod_geo = Geoserver( + service_url=prod_url, + username=settings.PROD_GEOSERVER_USERNAME, + password=settings.PROD_GEOSERVER_PASSWORD, + ) + prod_response = _push_vector_to_geoserver_instance( + prod_geo, path, layer_name, workspace, file_type + ) + prod_ok = isinstance(prod_response, dict) and prod_response.get( + "status_code" + ) in (200, 201) + _log.info( + "Pushed vector %s to prod GeoServer (%s) (ok=%s).", + layer_name, + prod_url, + prod_ok, + ) + except Exception as e: + prod_ok = False + prod_response = {"error": str(e)} + _log.error("Failed to push vector %s to prod GeoServer: %s", layer_name, e) + + overall_ok = local_ok and (prod_ok if prod_ok is not None else True) + failure_status = None + if isinstance(prod_response, dict): + failure_status = prod_response.get("status_code") + if failure_status is None and isinstance(local_response, dict): + failure_status = local_response.get("status_code") + + return { + "status_code": 201 if overall_ok else (failure_status or 500), + "local_ok": local_ok, + "prod_ok": prod_ok, + "local_response": local_response, + "prod_response": prod_response, + } + + +def push_local_raster_to_geoserver(file_path, layer_name, workspace, style_name=None): + import logging + from django.conf import settings + from utilities.geoserver_utils import Geoserver + + _log = logging.getLogger(__name__) + + local_geo = Geoserver() + upload_response, style_response = _push_raster_to_geoserver_instance( + local_geo, file_path, layer_name, workspace, style_name + ) + local_layer_ok = _verify_raster_layer(local_geo, workspace, layer_name) + _log.info( + "Pushed raster %s to local GeoServer (layer_exists=%s).", + layer_name, + local_layer_ok, + ) + + prod_url = getattr(settings, "PROD_GEOSERVER_URL", "") + if prod_url: + try: + prod_geo = Geoserver( + service_url=prod_url, + username=settings.PROD_GEOSERVER_USERNAME, + password=settings.PROD_GEOSERVER_PASSWORD, + ) + prod_upload, prod_style = _push_raster_to_geoserver_instance( + prod_geo, file_path, layer_name, workspace, style_name + ) + prod_layer_ok = _verify_raster_layer(prod_geo, workspace, layer_name) + _log.info( + "Pushed raster %s to prod GeoServer (%s) " + "(layer_exists=%s, upload=%s, style=%s).", + layer_name, + prod_url, + prod_layer_ok, + prod_upload, + prod_style, + ) + if not prod_layer_ok: + _log.error( + "Prod raster push for %s created store but NO layer. " + "Coverage auto-config likely failed on prod GeoServer.", + layer_name, + ) + except Exception as e: + _log.error("Failed to push raster %s to prod GeoServer: %s", layer_name, e) + + return upload_response, style_response + + + +def compute_pixel_area_grid(transform, height, width, crs): + if crs is None: + raise ValueError("Raster CRS is missing; cannot compute pixel areas.") + + if getattr(crs, "is_geographic", False): + lon_width_radians = np.deg2rad(abs(transform.a)) + row_indices = np.arange(height, dtype=np.float64) + lat_top = transform.f + (row_indices * transform.e) + lat_bottom = lat_top + transform.e + earth_radius_m = 6378137.0 + row_areas = (earth_radius_m**2) * lon_width_radians * np.abs( + np.sin(np.deg2rad(lat_top)) - np.sin(np.deg2rad(lat_bottom)) + ) + return np.broadcast_to(row_areas[:, None], (height, width)) + + pixel_area = abs(transform.a * transform.e) + return np.full((height, width), pixel_area, dtype=np.float64) + + + +def compute_categorical_raster_areas_for_watersheds( + watersheds_gdf, + raster_path, + class_definitions, +): + ensure_file_exists(raster_path, "Categorical raster") + + with rasterio.open(raster_path) as src: + working_gdf = watersheds_gdf.copy() + if working_gdf.crs is None: + raise ValueError( + "Watershed CRS is missing; cannot align with categorical raster." + ) + if src.crs and working_gdf.crs != src.crs: + working_gdf = working_gdf.to_crs(src.crs) + + nodata = src.nodata + computed_rows = [] + empty_result = { + class_definition["label"]: 0.0 + for class_definition in class_definitions + } + + total = len(working_gdf) + for index, row in enumerate(working_gdf.itertuples(index=False), start=1): + geom = row.geometry + if geom is None or geom.is_empty: + computed_rows.append(empty_result.copy()) + continue + + try: + clipped, clipped_transform = mask( + src, + [mapping(geom)], + crop=True, + filled=False, + ) + except ValueError: + computed_rows.append(empty_result.copy()) + continue + + data = clipped[0] + if data.size == 0: + computed_rows.append(empty_result.copy()) + continue + + values = np.asarray(data, dtype=np.float64) + values = np.where(np.isfinite(values), values, 0) + values = np.rint(values).astype(np.int16, copy=False) + valid_mask = ~np.ma.getmaskarray(data) + if nodata is not None and not np.isnan(nodata): + valid_mask &= values != int(round(nodata)) + + pixel_area_ha = ( + compute_pixel_area_grid( + transform=clipped_transform, + height=values.shape[0], + width=values.shape[1], + crs=src.crs, + ) + * 0.0001 + ) + + row_result = {} + for class_definition in class_definitions: + raw_values = class_definition.get("values", class_definition.get("value")) + if isinstance(raw_values, (list, tuple, set, np.ndarray)): + class_values = list(raw_values) + else: + class_values = [raw_values] + class_mask = valid_mask & np.isin(values, class_values) + row_result[class_definition["label"]] = float( + pixel_area_ha[class_mask].sum() + ) + + computed_rows.append(row_result) + + if index % 200 == 0 or index == total: + print( + f"Computed categorical raster areas for {index}/{total} watersheds" + ) + + result = watersheds_gdf.copy() + computed_df = pd.DataFrame(computed_rows) + for column in computed_df.columns: + result[column] = computed_df[column].values + return result + + +def compute_union_categorical_area_across_rasters_for_watersheds( + watersheds_gdf, + raster_paths, + class_values, + output_column, +): + if not raster_paths: + raise ValueError("At least one raster path is required for union area computation.") + + for raster_path in raster_paths: + ensure_file_exists(raster_path, "Categorical raster") + + if isinstance(class_values, (list, tuple, set, np.ndarray)): + class_values = list(class_values) + else: + class_values = [class_values] + + with ExitStack() as stack: + sources = [stack.enter_context(rasterio.open(path)) for path in raster_paths] + aligned_gdfs = [] + for src in sources: + aligned = watersheds_gdf.copy() + if aligned.crs is None: + raise ValueError( + "Watershed CRS is missing; cannot align with categorical raster." + ) + if src.crs and aligned.crs != src.crs: + aligned = aligned.to_crs(src.crs) + aligned_gdfs.append(aligned) + + computed_rows = [] + total = len(watersheds_gdf) + + for index in range(total): + union_mask = None + union_pixel_area_ha = None + + for src, aligned_gdf in zip(sources, aligned_gdfs): + geom = aligned_gdf.iloc[index].geometry + if geom is None or geom.is_empty: + continue + + try: + clipped, clipped_transform = mask( + src, + [mapping(geom)], + crop=True, + filled=False, + ) + except ValueError: + continue + + data = clipped[0] + if data.size == 0: + continue + + values = np.asarray(data, dtype=np.float64) + values = np.where(np.isfinite(values), values, 0) + values = np.rint(values).astype(np.int16, copy=False) + + valid_mask = ~np.ma.getmaskarray(data) + nodata = src.nodata + if nodata is not None and not np.isnan(nodata): + valid_mask &= values != int(round(nodata)) + + class_mask = valid_mask & np.isin(values, class_values) + if union_mask is None: + union_mask = class_mask + union_pixel_area_ha = ( + compute_pixel_area_grid( + transform=clipped_transform, + height=values.shape[0], + width=values.shape[1], + crs=src.crs, + ) + * 0.0001 + ) + else: + if union_mask.shape != class_mask.shape: + raise ValueError( + "Local categorical rasters do not align for union area computation." + ) + union_mask |= class_mask + + if union_mask is None or union_pixel_area_ha is None: + computed_rows.append({output_column: 0.0}) + else: + computed_rows.append( + {output_column: float(union_pixel_area_ha[union_mask].sum())} + ) + + if (index + 1) % 200 == 0 or (index + 1) == total: + print( + f"Computed union categorical raster areas for {index + 1}/{total} watersheds" + ) + + result = watersheds_gdf.copy() + computed_df = pd.DataFrame(computed_rows) + result[output_column] = computed_df[output_column].values + return result + + +def get_union_geometry(gdf): + if hasattr(gdf.geometry, "union_all"): + return gdf.geometry.union_all() + return gdf.geometry.unary_union + + +def resolve_clipped_terrain_raster_path( + state, + district, + block, + clipped_raster_dir, +): + layer_stub = f"{_slug(district, 'unknown_district')}_{_slug(block, 'unknown_tehsil')}_terrain_raster" + raster_path = ( + Path(clipped_raster_dir) + / _slug(state, "unknown_state") + / _slug(district, "unknown_district") + / _slug(block, "unknown_tehsil") + / f"{layer_stub}.tif" + ) + ensure_file_exists(raster_path, "Clipped terrain raster") + return str(raster_path) + + +def _compute_cluster_id( + slopy_prop, + plain_prop, + ridge_prop, + valley_prop, + hill_slopes_prop, +): + feature_vector = np.array( + [slopy_prop, plain_prop, ridge_prop, valley_prop, hill_slopes_prop], + dtype=np.float64, + ) + distances = np.sum((TERRAIN_CLUSTER_CENTROIDS - feature_vector) ** 2, axis=1) + return int(np.argmin(distances)) + + +def _fraction(values, valid_mask, class_values): + total = int(valid_mask.sum()) + if total == 0: + return 0.0 + class_mask = np.isin(values, list(class_values)) + return float((class_mask & valid_mask).sum()) / float(total) + + +def compute_terrain_properties_for_watersheds(watersheds_gdf, raster_path): + with rasterio.open(raster_path) as src: + working_gdf = watersheds_gdf.copy() + if working_gdf.crs is None: + raise ValueError( + "Watershed CRS is missing; cannot align with raster CRS." + ) + if src.crs and working_gdf.crs != src.crs: + working_gdf = working_gdf.to_crs(src.crs) + + nodata = src.nodata + computed_rows = [] + + total = len(working_gdf) + for index, row in enumerate(working_gdf.itertuples(index=False), start=1): + geom = row.geometry + if geom is None or geom.is_empty: + computed_rows.append( + { + "plain_area": 0.0, + "valley_area": 0.0, + "hill_slopes_area": 0.0, + "ridge_area": 0.0, + "slopy_area": 0.0, + "terrainClusters": -1, + } + ) + continue + + try: + clipped, _ = mask(src, [mapping(geom)], crop=True, filled=True) + except ValueError: + computed_rows.append( + { + "plain_area": 0.0, + "valley_area": 0.0, + "hill_slopes_area": 0.0, + "ridge_area": 0.0, + "slopy_area": 0.0, + "terrainClusters": -1, + } + ) + continue + + values = clipped[0] + if values.size == 0: + computed_rows.append( + { + "plain_area": 0.0, + "valley_area": 0.0, + "hill_slopes_area": 0.0, + "ridge_area": 0.0, + "slopy_area": 0.0, + "terrainClusters": -1, + } + ) + continue + + values = np.rint(values).astype(np.int16, copy=False) + valid_mask = np.ones(values.shape, dtype=bool) + if nodata is not None and not np.isnan(nodata): + valid_mask &= values != int(round(nodata)) + valid_mask &= values != 0 + + plain_prop = _fraction(values, valid_mask, PLAIN_CLASSES) + valley_prop = _fraction(values, valid_mask, VALLEY_CLASSES) + hill_slopes_prop = _fraction( + values, valid_mask, HILL_SLOPES_CLASSES + ) + ridge_prop = _fraction(values, valid_mask, RIDGE_CLASSES) + slopy_prop = _fraction(values, valid_mask, SLOPY_CLASSES) + + if valid_mask.sum() == 0: + cluster_id = -1 + else: + cluster_id = _compute_cluster_id( + slopy_prop=slopy_prop, + plain_prop=plain_prop, + ridge_prop=ridge_prop, + valley_prop=valley_prop, + hill_slopes_prop=hill_slopes_prop, + ) + + computed_rows.append( + { + "plain_area": plain_prop * 100.0, + "valley_area": valley_prop * 100.0, + "hill_slopes_area": hill_slopes_prop * 100.0, + "ridge_area": ridge_prop * 100.0, + "slopy_area": slopy_prop * 100.0, + "terrainClusters": cluster_id, + } + ) + + if index % 200 == 0 or index == total: + print( + f"Computed terrain properties for {index}/{total} watersheds" + ) + + result = watersheds_gdf.copy() + computed_df = pd.DataFrame(computed_rows) + for column in computed_df.columns: + result[column] = computed_df[column].values + return result + + +def resolve_lulc_raster_paths( + start_year, + end_year, + lulc_dir=LULC_BASE_DIR, +): + raster_paths = [] + for year in range(int(start_year), int(end_year) + 1): + raster_path = Path(lulc_dir) / f"lulc_v3_{year}_{year + 1}.tif" + ensure_file_exists(raster_path, f"LULC raster for {year}-{year + 1}") + raster_paths.append(str(raster_path)) + return raster_paths + + +def get_watershed_areas_in_hectares(watersheds_gdf): + if "area_in_ha" in watersheds_gdf.columns: + area_in_ha = pd.to_numeric( + watersheds_gdf["area_in_ha"], errors="coerce" + ) + if area_in_ha.notna().any(): + return area_in_ha + projected = watersheds_gdf.to_crs("EPSG:6933") + return projected.geometry.area / 10000.0 + + +def filter_large_watersheds( + watersheds_gdf, + min_watershed_area_ha=MIN_WATERSHED_AREA_HA, +): + area_in_ha = get_watershed_areas_in_hectares(watersheds_gdf) + filtered = watersheds_gdf.loc[area_in_ha > min_watershed_area_ha].copy() + filtered["area"] = area_in_ha.loc[filtered.index].astype(float) * 10000.0 + return filtered.reset_index(drop=True) + + +def resolve_aez_code( + watersheds_gdf, + aez_vector_path=AEZ_VECTOR_PATH, +): + ensure_file_exists(aez_vector_path, "AEZ vector") + + aez_gdf = read_validated_vector_file( + aez_vector_path, + f"AEZ vector has no valid geometries: {aez_vector_path}", + ) + + study_area = watersheds_gdf[["geometry"]].copy() + if study_area.crs is None: + raise ValueError("Watershed CRS is missing; cannot resolve AEZ.") + if aez_gdf.crs and study_area.crs != aez_gdf.crs: + study_area = study_area.to_crs(aez_gdf.crs) + + study_union = get_union_geometry(study_area) + intersecting_aez = aez_gdf.loc[aez_gdf.intersects(study_union)].copy() + if intersecting_aez.empty: + raise ValueError("No AEZ polygon intersects the watershed study area.") + + intersecting_aez["overlap_area"] = intersecting_aez.geometry.intersection( + study_union + ).area + return int( + intersecting_aez.sort_values("overlap_area", ascending=False).iloc[0][ + "ae_regcode" + ] + ) + + +def compute_mode_lulc_array(reprojected_arrays, lulc_classes=LULC_CLASSES): + stack = np.rint(np.stack(reprojected_arrays, axis=0)).astype( + np.int16, + copy=False, + ) + valid_mask = np.isfinite(stack) & (stack > 0) + counts = np.zeros( + (len(lulc_classes), stack.shape[1], stack.shape[2]), + dtype=np.uint16, + ) + + for index, lulc_class in enumerate(lulc_classes): + counts[index] = np.sum((stack == lulc_class) & valid_mask, axis=0) + + mode_index = np.argmax(counts, axis=0) + mode_values = lulc_classes[mode_index] + max_counts = np.max(counts, axis=0) + mode_values[max_counts == 0] = 0 + return mode_values + + +def get_compute_mode(request, default="local"): + compute = str(request.data.get("compute") or default).strip().lower() + if compute not in VALID_COMPUTE_TYPES: + raise ValueError("compute must be either 'gee' or 'local'") + return compute + + +def select_compute_task(compute, gee_task, local_task): + return gee_task if compute == "gee" else local_task + + +def read_geojson_with_string_coords(path, mask_gdf): + """ + Safely reads a GeoJSON file with malformed string coordinates that cause fiona to crash. + Pre-filters using the bounding box of mask_gdf to prevent high memory usage. + """ + import json + with open(path, 'r') as f: + data = json.load(f) + + def _convert_coords(coords): + if not coords: + return coords + if isinstance(coords[0], (list, tuple)): + return [_convert_coords(c) for c in coords] + return [float(c) for c in coords] + + minx, miny, maxx, maxy = mask_gdf.total_bounds + buffer_deg = 0.05 + + def _is_roughly_in_bounds(coords): + if not coords: return False + if isinstance(coords[0], (list, tuple)): + for c in coords: + if _is_roughly_in_bounds(c): return True + return False + else: + try: + x, y = float(coords[0]), float(coords[1]) + return (minx - buffer_deg <= x <= maxx + buffer_deg) and (miny - buffer_deg <= y <= maxy + buffer_deg) + except Exception: + return True + + filtered_features = [] + for feature in data.get("features", []): + geom = feature.get("geometry") + if geom and "coordinates" in geom: + if _is_roughly_in_bounds(geom["coordinates"]): + try: + geom["coordinates"] = _convert_coords(geom["coordinates"]) + filtered_features.append(feature) + except Exception: + pass + + return gpd.GeoDataFrame.from_features(filtered_features, crs="EPSG:4326") + diff --git a/computing/lulc/lulc_v3.py b/computing/lulc/lulc_v3.py index 5be3bcd8..bc5c9292 100644 --- a/computing/lulc/lulc_v3.py +++ b/computing/lulc/lulc_v3.py @@ -235,39 +235,36 @@ def sync_lulc_to_geoserver( asset_suffix=None, ): print("Syncing lulc to geoserver") - lulc_workspaces = ["LULC_level_1", "LULC_level_2", "LULC_level_3"] + # lulc_workspaces = ["LULC_level_1", "LULC_level_2", "LULC_level_3"] layer_at_geoserver = False for i in range(0, len(final_output_filename_array_new)): - name_arr = final_output_filename_array_new[i].split( - "_20" - ) # TODO: better logic than this + name_arr = final_output_filename_array_new[i].split("_20") s_year = name_arr[1][:2] e_year = name_arr[2][:2] gcs_file_name = "LULC_" + s_year + "_" + e_year + "_" + name_arr[0] print("Syncing " + gcs_file_name + " to geoserver") - for workspace in lulc_workspaces: - suff = workspace.replace("LULC", "") - style = workspace.lower() + "_style" - if block_name: - layer_name = ( - "LULC_" - + s_year - + "_" - + e_year - + "_" - + valid_gee_text(district_name.lower()) - + "_" - + valid_gee_text(block_name.lower()) - + suff - ) - else: - layer_name = f"LULC_{s_year}_{e_year}_{asset_suffix}_{suff}" - - res = sync_raster_gcs_to_geoserver( - workspace, gcs_file_name, layer_name, style + # for workspace in lulc_workspaces: + workspace = "LULC_level_3" + suff = workspace.replace("LULC", "") + style = workspace.lower() + "_style" + if block_name: + layer_name = ( + "LULC_" + + s_year + + "_" + + e_year + + "_" + + valid_gee_text(district_name.lower()) + + "_" + + valid_gee_text(block_name.lower()) + + suff ) - if res and layer_ids: - update_layer_sync_status(layer_id=layer_ids[i], sync_to_geoserver=True) - print("geoserver flag is updated") - layer_at_geoserver = True + else: + layer_name = f"LULC_{s_year}_{e_year}_{asset_suffix}_{suff}" + + res = sync_raster_gcs_to_geoserver(workspace, gcs_file_name, layer_name, style) + if res and layer_ids: + update_layer_sync_status(layer_id=layer_ids[i], sync_to_geoserver=True) + print("geoserver flag is updated") + layer_at_geoserver = True return layer_at_geoserver diff --git a/computing/lulc/lulc_v3_local.py b/computing/lulc/lulc_v3_local.py new file mode 100644 index 00000000..bf9d1726 --- /dev/null +++ b/computing/lulc/lulc_v3_local.py @@ -0,0 +1,263 @@ +from computing.config_loader import LULC_V3_OUTPUT_DIR as LOCAL_OUTPUT_BASE_DIR +from computing.local_compute_helper import ( + LULC_BASE_DIR, + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + build_output_raster_path, + clip_raster_with_roi, + load_precomputed_roi, + push_local_raster_to_geoserver, + read_validated_vector_file, + resolve_lulc_raster_paths, +) +from computing.models import Dataset +from computing.STAC_specs import generate_STAC_layerwise +from computing.utils import save_layer_info_to_db, update_layer_sync_status +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text + +GEOSERVER_WORKSPACE = "LULC_level_3" +GEOSERVER_STYLE = "lulc_level_3_style" +LOCAL_ALGORITHM = "local_lulc_v3_clip" +LOCAL_ALGORITHM_VERSION = "local-1.0" + + +def _slug(value, fallback): + if value is None: + return fallback + text = str(value).strip().lower() + if not text: + return fallback + return valid_gee_text(text) or fallback + + +def _resolve_roi(state, district, block, roi_path, precomputed_roi_dir): + if state and district and block: + return load_precomputed_roi( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + + if not roi_path: + raise ValueError( + "For non state/district/block runs, `roi_path` must be provided." + ) + + return read_validated_vector_file( + roi_path, + f"ROI file has no valid geometries: {roi_path}", + ) + + +def _resolve_filename_prefix(district, block, asset_suffix): + if district and block: + return f"{_slug(district, 'unknown_district')}_{_slug(block, 'unknown_block')}" + if asset_suffix is None or not str(asset_suffix).strip(): + raise ValueError( + "For non state/district/block runs, `asset_suffix` must be provided." + ) + return _slug(asset_suffix, "custom") + + +def _build_output_stub(filename_prefix, start_year): + return f"{filename_prefix}_{start_year}-07-01_{start_year + 1}-06-30_LULCmap_10m" + + +def _build_layer_name(start_year, end_year, filename_prefix): + return f"LULC_{start_year}_{end_year}_{filename_prefix}_level_3" + + +def _resolve_dataset_name(): + return ( + "LULC_v3" if Dataset.objects.filter(name="LULC_v3").exists() else "LULC_level_3" + ) + + +def _sync_lulc_stac(layer_id, state, district, block, start_year): + + layer_stac_generated = generate_STAC_layerwise.generate_raster_stac( + state=state, + district=district, + block=block, + layer_name="land_use_land_cover_raster", + start_year=str(start_year), + ) + update_layer_sync_status( + layer_id=layer_id, + is_stac_specs_generated=layer_stac_generated, + ) + + +def run_lulc_v3_local( + state=None, + district=None, + block=None, + start_year=None, + end_year=None, + roi_path=None, + asset_suffix=None, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + lulc_dir=LULC_BASE_DIR, + push_to_geoserver=True, + sync_layer_metadata=True, +): + state = str(state).strip().lower() if state else None + district = str(district).strip().lower() if district else None + block = str(block).strip().lower() if block else None + start_year = int(start_year) + end_year = int(end_year) + + if end_year < start_year: + raise ValueError("end_year must be greater than or equal to start_year") + + roi_gdf = _resolve_roi( + state=state, + district=district, + block=block, + roi_path=roi_path, + precomputed_roi_dir=precomputed_roi_dir, + ) + filename_prefix = _resolve_filename_prefix(district, block, asset_suffix) + raster_paths = resolve_lulc_raster_paths( + start_year=start_year, + end_year=end_year, + lulc_dir=lulc_dir, + ) + + layer_at_geoserver = True + is_admin_run = bool(state and district and block) + + for current_year, raster_path in zip(range(start_year, end_year + 1), raster_paths): + year_start = str(current_year)[2:] + year_end = str(current_year + 1)[2:] + output_stub = _build_output_stub(filename_prefix, current_year) + output_path = build_output_raster_path( + layer_name=output_stub, + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + state=state, + district=district, + block=block, + custom_subdir=_slug(asset_suffix, "custom"), + block_fallback="unknown_block", + ) + clipped_raster_path = clip_raster_with_roi( + roi_gdf=roi_gdf, + raster_path=raster_path, + output_path=output_path, + raster_label=f"LULC raster for {current_year}-{current_year + 1}", + ) + print(f"Saved local LULC raster: {clipped_raster_path}") + + layer_name = _build_layer_name( + start_year=year_start, + end_year=year_end, + filename_prefix=filename_prefix, + ) + print(f"Prepared local LULC layer {GEOSERVER_WORKSPACE}: {layer_name}") + + layer_id = None + if sync_layer_metadata and is_admin_run: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=clipped_raster_path, + dataset_name=_resolve_dataset_name(), + misc={ + "start_year": start_year, + "end_year": end_year, + "is_generated_locally": True, + }, + algorithm=LOCAL_ALGORITHM, + algorithm_version=LOCAL_ALGORITHM_VERSION, + ) + + if not push_to_geoserver: + continue + + try: + upload_res, style_res = push_local_raster_to_geoserver( + file_path=clipped_raster_path, + layer_name=layer_name, + workspace=GEOSERVER_WORKSPACE, + style_name=GEOSERVER_STYLE, + ) + print(f"GeoServer upload response for {layer_name}: {upload_res}") + print(f"GeoServer style response for {layer_name}: {style_res}") + except Exception as error: + print(f"Failed to sync local LULC raster {layer_name}: {error}") + layer_at_geoserver = False + continue + + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + _sync_lulc_stac( + layer_id=layer_id, + state=state, + district=district, + block=block, + start_year=current_year, + ) + + return layer_at_geoserver if push_to_geoserver else True + + +def _clip_lulc_v3_local_task( + state=None, + district=None, + block=None, + start_year=None, + end_year=None, + gee_account_id=None, + roi_path=None, + asset_folder=None, + asset_suffix=None, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + app_type="MWS", +): + _ = gee_account_id, asset_folder, app_type + return run_lulc_v3_local( + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + roi_path=roi_path, + asset_suffix=asset_suffix, + precomputed_roi_dir=precomputed_roi_dir, + push_to_geoserver=True, + sync_layer_metadata=True, + ) + + +@app.task(bind=True) +def clip_lulc_v3( + self, + state=None, + district=None, + block=None, + start_year=None, + end_year=None, + gee_account_id=None, + roi_path=None, + asset_folder=None, + asset_suffix=None, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + app_type="MWS", +): + _ = self + return _clip_lulc_v3_local_task( + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + gee_account_id=gee_account_id, + roi_path=roi_path, + asset_folder=asset_folder, + asset_suffix=asset_suffix, + precomputed_roi_dir=precomputed_roi_dir, + app_type=app_type, + ) diff --git a/computing/lulc/lulc_vector_local.py b/computing/lulc/lulc_vector_local.py new file mode 100644 index 00000000..689f1a8a --- /dev/null +++ b/computing/lulc/lulc_vector_local.py @@ -0,0 +1,208 @@ +import logging +import os + +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text + +from computing.config_loader import LULC_VECTOR_OUTPUT_DIR as LOCAL_OUTPUT_BASE_DIR +from computing.local_compute_helper import ( + LULC_BASE_DIR, + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + build_output_vector_path, + compute_categorical_raster_areas_for_watersheds, + load_precomputed_watersheds, + push_local_vector_to_geoserver, + resolve_lulc_raster_paths, + write_vector_output, +) +from computing.utils import ( + save_layer_info_to_db, + update_layer_sync_status, +) + +logger = logging.getLogger(__name__) + +GEOSERVER_WORKSPACE = "lulc_vector" +LOCAL_ALGORITHM = "local_lulc_vector" +LOCAL_ALGORITHM_VERSION = "local-1.0" + +LULC_VECTOR_CLASS_DEFINITIONS = ( + {"value": 1, "label": "built-up_area_"}, + {"value": 2, "label": "k_water_area_"}, + {"value": 3, "label": "kr_water_area_"}, + {"value": 4, "label": "krz_water_area_"}, + {"value": 5, "label": "cropland_area_"}, + {"value": 6, "label": "tree_forest_area_"}, + {"value": 7, "label": "barrenlands_area_"}, + {"value": 8, "label": "single_kharif_cropped_area_"}, + {"value": 9, "label": "single_non_kharif_cropped_area_"}, + {"value": 10, "label": "doubly_cropped_area_"}, + {"value": 11, "label": "triply_cropped_area_"}, + {"value": 12, "label": "shrub_scrub_area_"}, +) + + +def _slug(value, fallback): + text = str(value).strip().lower() + return valid_gee_text(text) or fallback + + +def _layer_name(district, block): + return f"lulc_vector_{_slug(district, 'unknown_district')}_{_slug(block, 'unknown_block')}" + + +def _year_class_definitions(year): + return [ + { + "value": class_definition["value"], + "label": f"{class_definition['label']}{year}", + } + for class_definition in LULC_VECTOR_CLASS_DEFINITIONS + ] + + +def run_lulc_vector_local( + state, + district, + block, + start_year, + end_year, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + lulc_dir=LULC_BASE_DIR, + push_to_geoserver=True, + sync_layer_metadata=True, +): + state = str(state).strip().lower() + district = str(district).strip().lower() + block = str(block).strip().lower() + start_year = int(start_year) + end_year = int(end_year) + + if end_year < start_year: + raise ValueError("end_year must be greater than or equal to start_year") + + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + logger.info("Watershed boundary source: %s", watershed_source) + + layer_name = _layer_name(district, block) + result_gdf = watersheds_gdf.copy() + raster_paths = resolve_lulc_raster_paths( + start_year=start_year, + end_year=end_year, + lulc_dir=lulc_dir, + ) + + for year, raster_path in zip(range(start_year, end_year + 1), raster_paths): + logger.info("Computing LULC vector properties for %d-%d: %s", year, year + 1, raster_path) + year_result = compute_categorical_raster_areas_for_watersheds( + watersheds_gdf=result_gdf, + raster_path=raster_path, + class_definitions=_year_class_definitions(year), + ) + for column in year_result.columns: + if column not in result_gdf.columns: + result_gdf[column] = year_result[column] + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + block_fallback="unknown_block", + ) + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + logger.info("Saved local LULC vector: %s", asset_id) + + geoserver_ok = False + geoserver_response = None + if push_to_geoserver: + geoserver_response = push_local_vector_to_geoserver( + path=os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + geoserver_ok = ( + isinstance(geoserver_response, dict) + and geoserver_response.get("status_code") in (200, 201) + ) + if geoserver_ok: + logger.info("GeoServer upload succeeded for layer %s", layer_name) + else: + logger.error( + "GeoServer upload failed for layer %s: %s", layer_name, geoserver_response + ) + + layer_id = None + if sync_layer_metadata: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="LULC", + misc={ + "start_year": start_year, + "end_year": end_year, + "is_generated_locally": True, + }, + algorithm=LOCAL_ALGORITHM, + algorithm_version=LOCAL_ALGORITHM_VERSION, + ) + logger.info("Saved layer metadata to DB: layer_id=%s", layer_id) + if layer_id and geoserver_ok: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + + return geoserver_ok if push_to_geoserver else True + + +def _vectorise_lulc_local_task( + state, + district, + block, + start_year, + end_year, + gee_account_id=None, +): + _ = gee_account_id + return run_lulc_vector_local( + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + push_to_geoserver=True, + sync_layer_metadata=True, + ) + + +@app.task(bind=True) +def vectorise_lulc( + self, + state, + district, + block, + start_year, + end_year, + gee_account_id=None, +): + _ = self + return _vectorise_lulc_local_task( + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + gee_account_id=gee_account_id, + ) diff --git a/computing/lulc_X_terrain/lulc_on_plain_cluster_local.py b/computing/lulc_X_terrain/lulc_on_plain_cluster_local.py new file mode 100644 index 00000000..f15dc51a --- /dev/null +++ b/computing/lulc_X_terrain/lulc_on_plain_cluster_local.py @@ -0,0 +1,405 @@ +import logging +import os +from contextlib import ExitStack + +import numpy as np +import pandas as pd +import rasterio +from rasterio.mask import mask +from rasterio.warp import Resampling, reproject +from utilities.gee_utils import valid_gee_text + +from nrm_app.celery import app + +from computing.config_loader import LULC_PLAIN_CLUSTER_OUTPUT_DIR as LOCAL_OUTPUT_BASE_DIR +from computing.local_compute_helper import ( + AEZ_VECTOR_PATH, + LULC_BASE_DIR, + MIN_WATERSHED_AREA_HA, + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + TERRAIN_RASTER_PATH, + build_output_vector_path as _build_output_vector_path, + compute_mode_lulc_array as _compute_mode_lulc_array, + compute_terrain_properties_for_watersheds, + ensure_file_exists as _ensure_file_exists, + filter_large_watersheds as _filter_large_watersheds, + load_precomputed_watersheds as _load_precomputed_watersheds, + resolve_aez_code as _resolve_aez_code, + resolve_lulc_raster_paths as _resolve_lulc_raster_paths, + validate_geometry as _validate_geometry, + write_vector_output as _write_output_vector, +) +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) + +from .utils import aez_lulcXterrain_cluster_centroids + +logger = logging.getLogger(__name__) + +GEOSERVER_WORKSPACE = "terrain_lulc" + +PLAIN_LULC_FIELD_MAPPING = { + "barren": 7, + "double_crop": 10, + "shrubs_scrubs": 12, + "sing_crop": 8, + "sing_non_kharif_crop": 9, + "forest": 6, + "triple_crop": 11, +} + + +def _compute_plain_lulc_properties_for_feature(geometry, terrain_src, lulc_sources): + try: + terrain_data, terrain_transform = mask( + terrain_src, + [geometry.__geo_interface__], + crop=True, + filled=True, + ) + except ValueError: + return None + + terrain_values = np.rint(terrain_data[0]).astype(np.int16, copy=False) + if terrain_values.size == 0: + return None + + terrain_valid_mask = np.ones(terrain_values.shape, dtype=bool) + if terrain_src.nodata is not None: + terrain_valid_mask &= terrain_values != int(round(terrain_src.nodata)) + terrain_valid_mask &= terrain_values > 0 + + if not terrain_valid_mask.any(): + return None + + reprojected_lulc = [] + for lulc_src in lulc_sources: + destination = np.zeros(terrain_values.shape, dtype=np.float32) + reproject( + source=rasterio.band(lulc_src, 1), + destination=destination, + src_transform=lulc_src.transform, + src_crs=lulc_src.crs, + src_nodata=lulc_src.nodata, + dst_transform=terrain_transform, + dst_crs=terrain_src.crs, + dst_nodata=0, + resampling=Resampling.nearest, + ) + reprojected_lulc.append(destination) + + lulc_mode = _compute_mode_lulc_array(reprojected_lulc) + plains_mask = terrain_valid_mask & (terrain_values == 5) + slopy_mask = terrain_valid_mask & (terrain_values == 6) + plain_plus_slope_pixels = int((plains_mask | slopy_mask).sum()) + + if plain_plus_slope_pixels == 0: + return { + "LxP_cluster": -1, + "clust_name": "No plain or slope pixels", + "barren": 0.0, + "double_crop": 0.0, + "shrubs_scrubs": 0.0, + "sing_crop": 0.0, + "sing_non_kharif_crop": 0.0, + "forest": 0.0, + "triple_crop": 0.0, + } + + proportions = {} + for field_name, lulc_class in PLAIN_LULC_FIELD_MAPPING.items(): + class_pixels = int((plains_mask & (lulc_mode == lulc_class)).sum()) + proportions[field_name] = class_pixels / float(plain_plus_slope_pixels) + + return proportions + + +def _assign_plain_clusters( + plain_watersheds_gdf, + terrain_raster_path, + plain_centroids, + lulc_raster_paths, +): + centroid_vectors = np.array( + [ + plain_centroids[str(index)]["cluster_vector"] + for index in range(len(plain_centroids)) + ], + dtype=np.float64, + ) + + computed_rows = [] + with rasterio.open(terrain_raster_path) as terrain_src, ExitStack() as stack: + lulc_sources = [ + stack.enter_context(rasterio.open(path)) + for path in lulc_raster_paths + ] + total = len(plain_watersheds_gdf) + + for index, row in enumerate( + plain_watersheds_gdf.itertuples(index=False), + start=1, + ): + properties = _compute_plain_lulc_properties_for_feature( + geometry=row.geometry, + terrain_src=terrain_src, + lulc_sources=lulc_sources, + ) + + if properties is None: + computed_rows.append( + { + "LxP_cluster": -1, + "clust_name": "No valid terrain pixels", + "barren": 0.0, + "double_crop": 0.0, + "shrubs_scrubs": 0.0, + "sing_crop": 0.0, + "sing_non_kharif_crop": 0.0, + "forest": 0.0, + "triple_crop": 0.0, + } + ) + elif properties.get("LxP_cluster") == -1: + computed_rows.append(properties) + else: + feature_vector = np.array( + [ + properties["barren"], + properties["double_crop"], + properties["shrubs_scrubs"], + properties["sing_crop"], + properties["sing_non_kharif_crop"], + properties["forest"], + properties["triple_crop"], + ], + dtype=np.float64, + ) + distances = np.sum( + (centroid_vectors - feature_vector) ** 2, + axis=1, + ) + cluster_index = int(np.argmin(distances)) + + computed_rows.append( + { + "LxP_cluster": cluster_index, + "clust_name": plain_centroids[str(cluster_index)][ + "cluster_name" + ], + "barren": properties["barren"] * 100.0, + "double_crop": properties["double_crop"] * 100.0, + "shrubs_scrubs": properties["shrubs_scrubs"] * 100.0, + "sing_crop": properties["sing_crop"] * 100.0, + "sing_non_kharif_crop": ( + properties["sing_non_kharif_crop"] * 100.0 + ), + "forest": properties["forest"] * 100.0, + "triple_crop": properties["triple_crop"] * 100.0, + } + ) + + if index % 200 == 0 or index == total: + logger.info("Computed plain LULC clusters for %d/%d watersheds", index, total) + + result = plain_watersheds_gdf.copy() + computed_df = pd.DataFrame(computed_rows) + for column in computed_df.columns: + result[column] = computed_df[column].values + return result + + +def run_lulc_on_plain_cluster_local( + state, + district, + block, + start_year, + end_year, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + aez_vector_path=AEZ_VECTOR_PATH, + lulc_dir=LULC_BASE_DIR, + terrain_raster_path=TERRAIN_RASTER_PATH, + push_to_geoserver=True, + sync_layer_metadata=True, +): + state = str(state).strip() + district = str(district).strip() + block = str(block).strip() + start_year = int(start_year) + end_year = int(end_year) + + if start_year > end_year: + raise ValueError("start_year cannot be greater than end_year") + + _ensure_file_exists(terrain_raster_path, "Terrain raster") + lulc_raster_paths = _resolve_lulc_raster_paths( + start_year=start_year, + end_year=end_year, + lulc_dir=lulc_dir, + ) + + watersheds_gdf, watershed_source = _load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + watersheds_gdf = _validate_geometry(watersheds_gdf) + if watersheds_gdf.empty: + raise ValueError("No valid watershed geometries found for local processing.") + + aez_code = _resolve_aez_code( + watersheds_gdf, + aez_vector_path=aez_vector_path, + ) + plain_centroids = aez_lulcXterrain_cluster_centroids[f"aez{aez_code}"]["plains"] + + filtered_watersheds = _filter_large_watersheds(watersheds_gdf) + if filtered_watersheds.empty: + raise ValueError( + f"No watersheds larger than {MIN_WATERSHED_AREA_HA} ha found for {state}/{district}/{block}." + ) + + terrain_classified = compute_terrain_properties_for_watersheds( + watersheds_gdf=filtered_watersheds, + raster_path=str(terrain_raster_path), + ) + terrain_classified["terrain_cluster"] = terrain_classified[ + "terrainClusters" + ].astype(int) + plain_watersheds = terrain_classified.loc[ + terrain_classified["terrain_cluster"] != 2 + ].copy() + if plain_watersheds.empty: + raise ValueError( + f"No plain-cluster watersheds found for {state}/{district}/{block}." + ) + + temp_columns = [ + "terrainClusters", + "plain_area", + "valley_area", + "hill_slopes_area", + "ridge_area", + "slopy_area", + ] + plain_watersheds.drop( + columns=[ + column + for column in temp_columns + if column in plain_watersheds.columns + ], + inplace=True, + ) + + result_gdf = _assign_plain_clusters( + plain_watersheds_gdf=plain_watersheds, + terrain_raster_path=str(terrain_raster_path), + plain_centroids=plain_centroids, + lulc_raster_paths=lulc_raster_paths, + ) + + layer_name = ( + f"{valid_gee_text(str(district).strip().lower())}_" + f"{valid_gee_text(str(block).strip().lower())}_lulc_plain" + ) + output_path = _build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + block_fallback="unknown_block", + ) + asset_id = _write_output_vector( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + logger.info("Saved local plain-cluster vector: %s", asset_id) + logger.info("Watershed boundary source: %s", watershed_source) + logger.info("Resolved AEZ code: %s", aez_code) + + geoserver_ok = False + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + geoserver_ok = ( + isinstance(geoserver_response, dict) + and geoserver_response.get("status_code") in (200, 201) + ) + if geoserver_ok: + logger.info("GeoServer upload succeeded for layer %s", layer_name) + else: + logger.error( + "GeoServer upload failed for layer %s: %s", layer_name, geoserver_response + ) + + layer_id = None + if sync_layer_metadata: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Terrain LULC", + misc={ + "start_year": start_year, + "end_year": end_year, + "is_generated_locally": True, + }, + ) + logger.info("Saved layer metadata to DB: layer_id=%s", layer_id) + if layer_id and geoserver_ok: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + + return geoserver_ok if push_to_geoserver else True + + +def _generate_lulc_on_plain_cluster_local_task( + state, + district, + block, + start_year, + end_year, + gee_account_id=None, +): + _ = gee_account_id + return run_lulc_on_plain_cluster_local( + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + push_to_geoserver=True, + sync_layer_metadata=True, + ) + + +@app.task(bind=True) +def lulc_on_plain_cluster_local( + self, + state, + district, + block, + start_year, + end_year, + gee_account_id=None, +): + _ = self + return _generate_lulc_on_plain_cluster_local_task( + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + gee_account_id=gee_account_id, + ) diff --git a/computing/lulc_X_terrain/lulc_on_slope_cluster_local.py b/computing/lulc_X_terrain/lulc_on_slope_cluster_local.py new file mode 100644 index 00000000..f306d57e --- /dev/null +++ b/computing/lulc_X_terrain/lulc_on_slope_cluster_local.py @@ -0,0 +1,399 @@ +import logging +import os + +import numpy as np +import pandas as pd +import rasterio +from rasterio.mask import mask +from rasterio.warp import Resampling, reproject +from utilities.gee_utils import valid_gee_text + +from nrm_app.celery import app + +from computing.config_loader import LULC_SLOPE_CLUSTER_OUTPUT_DIR as LOCAL_OUTPUT_BASE_DIR +from computing.local_compute_helper import ( + AEZ_VECTOR_PATH, + LULC_BASE_DIR, + MIN_WATERSHED_AREA_HA, + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + TERRAIN_RASTER_PATH, + build_output_vector_path as _build_output_vector_path, + compute_mode_lulc_array as _compute_mode_lulc_array, + compute_terrain_properties_for_watersheds, + ensure_file_exists as _ensure_file_exists, + filter_large_watersheds as _filter_large_watersheds, + load_precomputed_watersheds as _load_precomputed_watersheds, + resolve_aez_code as _resolve_aez_code, + resolve_lulc_raster_paths as _resolve_lulc_raster_paths, + validate_geometry as _validate_geometry, + write_vector_output as _write_output_vector, +) +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) + +from .utils import aez_lulcXterrain_cluster_centroids + +logger = logging.getLogger(__name__) + +GEOSERVER_WORKSPACE = "terrain_lulc" +SLOPE_LULC_FIELD_MAPPING = { + "barren": 7, + "double": 10, + "shrub_scrub": 12, + "sing_kharif": 8, + "sing_non_kharif": 9, + "forests": 6, + "triple": 11, +} + + +def _compute_slope_lulc_properties_for_feature(geometry, terrain_src, lulc_sources): + try: + terrain_data, terrain_transform = mask( + terrain_src, + [geometry.__geo_interface__], + crop=True, + filled=True, + ) + except ValueError: + return None + + terrain_values = np.rint(terrain_data[0]).astype(np.int16, copy=False) + if terrain_values.size == 0: + return None + + terrain_valid_mask = np.ones(terrain_values.shape, dtype=bool) + if terrain_src.nodata is not None: + terrain_valid_mask &= terrain_values != int(round(terrain_src.nodata)) + terrain_valid_mask &= terrain_values > 0 + + if not terrain_valid_mask.any(): + return None + + reprojected_lulc = [] + for lulc_src in lulc_sources: + destination = np.zeros(terrain_values.shape, dtype=np.float32) + reproject( + source=rasterio.band(lulc_src, 1), + destination=destination, + src_transform=lulc_src.transform, + src_crs=lulc_src.crs, + src_nodata=lulc_src.nodata, + dst_transform=terrain_transform, + dst_crs=terrain_src.crs, + dst_nodata=0, + resampling=Resampling.nearest, + ) + reprojected_lulc.append(destination) + + lulc_mode = _compute_mode_lulc_array(reprojected_lulc) + plains_mask = terrain_valid_mask & (terrain_values == 5) + slopy_mask = terrain_valid_mask & (terrain_values == 6) + plain_plus_slope_pixels = int((plains_mask | slopy_mask).sum()) + + if plain_plus_slope_pixels == 0: + return { + "LxS_cluster": -1, + "clust_name": "No plain or slope pixels", + "barren": 0.0, + "double": 0.0, + "shrub_scrub": 0.0, + "sing_kharif": 0.0, + "sing_non_kharif": 0.0, + "forests": 0.0, + "triple": 0.0, + } + + proportions = {} + for field_name, lulc_class in SLOPE_LULC_FIELD_MAPPING.items(): + class_pixels = int((slopy_mask & (lulc_mode == lulc_class)).sum()) + proportions[field_name] = class_pixels / float(plain_plus_slope_pixels) + + return proportions + + +def _assign_slope_clusters( + slope_watersheds_gdf, + terrain_raster_path, + slope_centroids, + lulc_raster_paths, +): + centroid_vectors = np.array( + [ + slope_centroids[str(index)]["cluster_vector"] + for index in range(len(slope_centroids)) + ], + dtype=np.float64, + ) + + computed_rows = [] + with rasterio.open(terrain_raster_path) as terrain_src: + lulc_sources = [rasterio.open(path) for path in lulc_raster_paths] + try: + total = len(slope_watersheds_gdf) + for index, row in enumerate( + slope_watersheds_gdf.itertuples(index=False), + start=1, + ): + properties = _compute_slope_lulc_properties_for_feature( + geometry=row.geometry, + terrain_src=terrain_src, + lulc_sources=lulc_sources, + ) + + if properties is None: + computed_rows.append( + { + "LxS_cluster": -1, + "clust_name": "No valid terrain pixels", + "barren": 0.0, + "double": 0.0, + "shrub_scrub": 0.0, + "sing_kharif": 0.0, + "sing_non_kharif": 0.0, + "forests": 0.0, + "triple": 0.0, + } + ) + elif properties.get("LxS_cluster") == -1: + computed_rows.append(properties) + else: + feature_vector = np.array( + [ + properties["barren"], + properties["shrub_scrub"], + properties["forests"], + ], + dtype=np.float64, + ) + distances = np.sum( + (centroid_vectors - feature_vector) ** 2, + axis=1, + ) + cluster_index = int(np.argmin(distances)) + + computed_rows.append( + { + "LxS_cluster": cluster_index, + "clust_name": slope_centroids[str(cluster_index)][ + "cluster_name" + ], + "barren": properties["barren"] * 100.0, + "double": properties["double"] * 100.0, + "shrub_scrub": properties["shrub_scrub"] * 100.0, + "sing_kharif": properties["sing_kharif"] * 100.0, + "sing_non_kharif": ( + properties["sing_non_kharif"] * 100.0 + ), + "forests": properties["forests"] * 100.0, + "triple": properties["triple"] * 100.0, + } + ) + + if index % 200 == 0 or index == total: + logger.info("Computed slope LULC clusters for %d/%d watersheds", index, total) + finally: + for lulc_src in lulc_sources: + lulc_src.close() + + result = slope_watersheds_gdf.copy() + computed_df = pd.DataFrame(computed_rows) + for column in computed_df.columns: + result[column] = computed_df[column].values + return result + + +def run_lulc_on_slope_cluster_local( + state, + district, + block, + start_year, + end_year, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + aez_vector_path=AEZ_VECTOR_PATH, + lulc_dir=LULC_BASE_DIR, + terrain_raster_path=TERRAIN_RASTER_PATH, + push_to_geoserver=True, + sync_layer_metadata=True, +): + state = str(state).strip() + district = str(district).strip() + block = str(block).strip() + start_year = int(start_year) + end_year = int(end_year) + + if start_year > end_year: + raise ValueError("start_year cannot be greater than end_year") + + _ensure_file_exists(terrain_raster_path, "Terrain raster") + lulc_raster_paths = _resolve_lulc_raster_paths( + start_year=start_year, + end_year=end_year, + lulc_dir=lulc_dir, + ) + + watersheds_gdf, watershed_source = _load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + watersheds_gdf = _validate_geometry(watersheds_gdf) + if watersheds_gdf.empty: + raise ValueError("No valid watershed geometries found for local processing.") + + aez_code = _resolve_aez_code( + watersheds_gdf, + aez_vector_path=aez_vector_path, + ) + slope_centroids = aez_lulcXterrain_cluster_centroids[f"aez{aez_code}"]["slopes"] + + filtered_watersheds = _filter_large_watersheds(watersheds_gdf) + if filtered_watersheds.empty: + raise ValueError( + f"No watersheds larger than {MIN_WATERSHED_AREA_HA} ha found for {state}/{district}/{block}." + ) + + terrain_classified = compute_terrain_properties_for_watersheds( + watersheds_gdf=filtered_watersheds, + raster_path=str(terrain_raster_path), + ) + terrain_classified["terrain_cluster"] = terrain_classified[ + "terrainClusters" + ].astype(int) + slope_watersheds = terrain_classified.loc[ + terrain_classified["terrain_cluster"].isin([0, 3]) + ].copy() + if slope_watersheds.empty: + raise ValueError( + f"No slope-cluster watersheds found for {state}/{district}/{block}." + ) + + temp_columns = [ + "terrainClusters", + "plain_area", + "valley_area", + "hill_slopes_area", + "ridge_area", + "slopy_area", + ] + slope_watersheds.drop( + columns=[ + column + for column in temp_columns + if column in slope_watersheds.columns + ], + inplace=True, + ) + + result_gdf = _assign_slope_clusters( + slope_watersheds_gdf=slope_watersheds, + terrain_raster_path=str(terrain_raster_path), + slope_centroids=slope_centroids, + lulc_raster_paths=lulc_raster_paths, + ) + + layer_name = ( + f"{valid_gee_text(str(district).strip().lower())}_" + f"{valid_gee_text(str(block).strip().lower())}_lulc_slope" + ) + output_path = _build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + block_fallback="unknown_block", + ) + asset_id = _write_output_vector( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + logger.info("Saved local slope-cluster vector: %s", asset_id) + logger.info("Watershed boundary source: %s", watershed_source) + logger.info("Resolved AEZ code: %s", aez_code) + + geoserver_ok = False + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + str(output_path.with_suffix("")), + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + geoserver_ok = ( + isinstance(geoserver_response, dict) + and geoserver_response.get("status_code") in (200, 201) + ) + if geoserver_ok: + logger.info("GeoServer upload succeeded for layer %s", layer_name) + else: + logger.error( + "GeoServer upload failed for layer %s: %s", layer_name, geoserver_response + ) + + layer_id = None + if sync_layer_metadata: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Terrain LULC", + misc={ + "start_year": start_year, + "end_year": end_year, + "is_generated_locally": True, + }, + ) + logger.info("Saved layer metadata to DB: layer_id=%s", layer_id) + if layer_id and geoserver_ok: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + + return geoserver_ok if push_to_geoserver else True + + +def _generate_lulc_on_slope_cluster_local_task( + state, + district, + block, + start_year, + end_year, + gee_account_id=None, +): + _ = gee_account_id + return run_lulc_on_slope_cluster_local( + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + push_to_geoserver=True, + sync_layer_metadata=True, + ) + + +@app.task(bind=True) +def lulc_on_slope_cluster_local( + self, + state, + district, + block, + start_year, + end_year, + gee_account_id=None, +): + _ = self + return _generate_lulc_on_slope_cluster_local_task( + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + gee_account_id=gee_account_id, + ) diff --git a/computing/management/commands/bulk_generate_layers.py b/computing/management/commands/bulk_generate_layers.py new file mode 100644 index 00000000..b711c57d --- /dev/null +++ b/computing/management/commands/bulk_generate_layers.py @@ -0,0 +1,179 @@ +from django.core.management.base import BaseCommand, CommandError + +from computing.bulk_layer_generation import ( + get_active_locations, + get_active_locations_from_api, + get_locally_generated_locations, + get_regeneration_dataset_names, + pipeline_names, + validate_pipeline, +) +from computing.tasks import bulk_generate_layer + + +DEFAULT_QUEUE = "layer_bulk" + + +class Command(BaseCommand): + help = "Queue a registered layer pipeline for active locations." + + def add_arguments(self, parser): + parser.add_argument("pipeline", nargs="?") + parser.add_argument("--all-active", action="store_true") + parser.add_argument( + "--regenerate-local", + action="store_true", + help=( + "Queue the pipeline for locations with at least one Layer row " + "where misc.is_generated_locally is true." + ), + ) + parser.add_argument( + "--from-prod-api", + action="store_true", + help=( + "Load active locations from PROD_BACKEND_URL instead of " + "the local database." + ), + ) + parser.add_argument("--state") + parser.add_argument("--district") + parser.add_argument( + "--block", + dest="blocks", + action="append", + help="Tehsil/block name. Repeat to select multiple blocks.", + ) + parser.add_argument("--limit", type=int) + parser.add_argument("--queue", default=DEFAULT_QUEUE) + parser.add_argument("--compute", choices=("local", "gee"), default="local") + parser.add_argument("--start-year", type=int) + parser.add_argument("--end-year", type=int) + parser.add_argument("--gee-account-id") + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--list-pipelines", action="store_true") + + overwrite_group = parser.add_mutually_exclusive_group() + overwrite_group.add_argument( + "--overwrite", dest="overwrite", action="store_true" + ) + overwrite_group.add_argument( + "--no-overwrite", dest="overwrite", action="store_false" + ) + parser.set_defaults(overwrite=True) + + def handle(self, *args, **options): + if options["list_pipelines"]: + self._list_pipelines(options["compute"]) + return + + pipeline = options["pipeline"] + if not pipeline: + raise CommandError("A pipeline name is required.") + try: + validate_pipeline( + pipeline, + compute=options["compute"], + start_year=options["start_year"], + end_year=options["end_year"], + gee_account_id=options["gee_account_id"], + overwrite=options["overwrite"], + ) + except ValueError as exc: + raise CommandError(str(exc)) from exc + + filters = { + name: options[name] + for name in ("state", "district") + if options[name] + } + if options["blocks"]: + filters["blocks"] = options["blocks"] + if ( + not options["all_active"] + and not options["regenerate_local"] + and not filters + ): + raise CommandError( + "Specify --all-active, --regenerate-local, or at least one of " + "--state, --district, or --block." + ) + if options["all_active"] and (options["regenerate_local"] or filters): + raise CommandError( + "--all-active cannot be combined with --regenerate-local or " + "location filters." + ) + if options["regenerate_local"] and options["compute"] != "local": + raise CommandError("--regenerate-local requires --compute=local.") + if options["regenerate_local"] and options["from_prod_api"]: + raise CommandError( + "--regenerate-local cannot be combined with --from-prod-api." + ) + if options["limit"] is not None and options["limit"] < 1: + raise CommandError("--limit must be greater than zero.") + queue = options["queue"].strip() + if not queue: + raise CommandError("--queue cannot be empty.") + + if options["regenerate_local"]: + location_loader = get_locally_generated_locations + elif options["from_prod_api"]: + location_loader = get_active_locations_from_api + else: + location_loader = get_active_locations + try: + if options["regenerate_local"]: + filters["dataset_names"] = get_regeneration_dataset_names(pipeline) + locations = location_loader( + **filters, + limit=options["limit"], + ) + except ValueError as exc: + raise CommandError(str(exc)) from exc + if not locations: + raise CommandError("No locations matched the requested scope.") + + action = "Would queue" if options["dry_run"] else "Queueing" + location_source = ( + "locally generated" if options["regenerate_local"] else "active" + ) + self.stdout.write( + f"{action} {options['compute']} pipeline '{pipeline}' for " + f"{len(locations)} {location_source} " + f"location(s) on queue '{queue}'." + ) + + for location in locations: + location_data = location.asdict() + label = ( + f"{location.state}/{location.district}/{location.block}" + ) + if options["dry_run"]: + self.stdout.write(f" {label}") + continue + + result = bulk_generate_layer.apply_async( + kwargs={ + "pipeline": pipeline, + "location": location_data, + "overwrite": options["overwrite"], + "compute": options["compute"], + "start_year": options["start_year"], + "end_year": options["end_year"], + "gee_account_id": options["gee_account_id"], + }, + queue=queue, + ) + self.stdout.write(f" {label}: {result.id}") + + if options["dry_run"]: + self.stdout.write(self.style.SUCCESS("Dry run complete; no tasks queued.")) + else: + self.stdout.write( + self.style.SUCCESS(f"Queued {len(locations)} task(s).") + ) + + def _list_pipelines(self, compute): + self.stdout.write(f"Registered {compute} pipelines:") + for name in pipeline_names(compute): + self.stdout.write(f" - {name}") diff --git a/computing/management/commands/local_compute_layer_setup.py b/computing/management/commands/local_compute_layer_setup.py new file mode 100644 index 00000000..38fa31e5 --- /dev/null +++ b/computing/management/commands/local_compute_layer_setup.py @@ -0,0 +1,148 @@ +import logging + +from django.core.management.base import BaseCommand, CommandError + +from computing.base_layer_setup import ( + DEFAULT_BASE_LAYERS, + _BASE_LAYER_ENSURERS, + _manifest_layer_groups, + setup_base_layers, +) + + +class Command(BaseCommand): + help = ( + "Set up required local compute layers into the local data directory. " + "Defaults to static_layers and periodic_layers." + ) + + def add_arguments(self, parser): + parser.add_argument( + "layers", + nargs="*", + help=( + "Layer names or groups to download, for example: terrain, mws, " + "lulc_v3, static_layers, periodic_layers. Defaults to the bootstrap set." + ), + ) + parser.add_argument( + "--ensure-soi-tehsil", + action="store_true", + help="Download the SOI tehsil layer if it is missing.", + ) + parser.add_argument( + "--ensure-admin-boundary", + action="store_true", + help="Download and extract admin boundary data if it is missing.", + ) + parser.add_argument( + "--ensure-lulc-rasters", + action="store_true", + help="Download missing LULC raster files.", + ) + parser.add_argument( + "--ensure-microwatershed", + action="store_true", + help="Download the microwatershed layer if it is missing.", + ) + parser.add_argument( + "--ensure-tehsil-watersheds", + action="store_true", + help="Generate per-tehsil watershed files if they are missing.", + ) + parser.add_argument( + "--geoserver", + action="store_true", + help=( + "Download watershed GPKGs from the mws GeoServer workspace for " + "active tehsils only." + ), + ) + parser.add_argument( + "--force", + action="store_true", + help="Replace existing tehsil watershed GPKGs.", + ) + parser.add_argument( + "--ensure-village-boundaries", + action="store_true", + help="Create the village boundaries directory if it is missing.", + ) + parser.add_argument( + "--list", + action="store_true", + help="List available layer selectors without downloading anything.", + ) + + def handle(self, *args, **options): + logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") + + if options["list"]: + self._print_available_layers() + return + + if options["geoserver"] and not options["ensure_tehsil_watersheds"]: + raise CommandError( + "--geoserver requires --ensure-tehsil-watersheds." + ) + if options["force"] and not options["ensure_tehsil_watersheds"]: + raise CommandError("--force requires --ensure-tehsil-watersheds.") + + layers = self._selected_layers(options) + self.stdout.write(f"Setting up local compute layers: {', '.join(layers)}") + + try: + setup_base_layers( + *layers, + geoserver=options["geoserver"], + force=options["force"], + ) + except (RuntimeError, ValueError) as exc: + raise CommandError(str(exc)) from exc + + self.stdout.write(self.style.SUCCESS("Local compute layer setup complete.")) + + def _selected_layers(self, options): + layers = list(options["layers"]) + flag_layers = { + "ensure_soi_tehsil": "soi_tehsil", + "ensure_admin_boundary": "admin_boundary", + "ensure_lulc_rasters": "lulc_rasters", + "ensure_microwatershed": "microwatershed", + "ensure_tehsil_watersheds": "tehsil_watersheds", + "ensure_village_boundaries": "village_boundaries", + } + + for option_name, layer_name in flag_layers.items(): + if options[option_name]: + layers.append(layer_name) + + return tuple(dict.fromkeys(layers)) or DEFAULT_BASE_LAYERS + + def _print_available_layers(self): + self.stdout.write("Downloadable manifest groups:") + for group_name, layers in _manifest_layer_groups().items(): + if not layers: + continue + self.stdout.write(f" {group_name}") + for layer in layers: + source = layer.get("source") + status = ( + "downloadable" + if source and layer.get("type") == "file" + else "manual" + ) + label = layer["name"] + if "year" in layer: + label = f"{label} {layer['year']}-{layer['year'] + 1}" + self.stdout.write(f" - {label} ({status})") + + self.stdout.write("") + self.stdout.write("Local/generated selectors:") + for selector in sorted(_BASE_LAYER_ENSURERS): + self.stdout.write(f" - {selector}") + + self.stdout.write("") + self.stdout.write("Local/generated flags:") + for selector in sorted(_BASE_LAYER_ENSURERS): + self.stdout.write(f" --ensure-{selector.replace('_', '-')}") diff --git a/computing/migrations/0004_layermapping.py b/computing/migrations/0004_layermapping.py new file mode 100644 index 00000000..d2243d7c --- /dev/null +++ b/computing/migrations/0004_layermapping.py @@ -0,0 +1,71 @@ +# Generated for LayerMapping STAC registry + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("computing", "0003_dataset_can_be_empty_dataset_is_active"), + ] + + operations = [ + migrations.CreateModel( + name="LayerMapping", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ( + "display_name", + models.CharField(blank=True, default="", max_length=255), + ), + ( + "layer_type", + models.CharField( + choices=[ + ("vector", "Vector"), + ("raster", "Raster"), + ("point", "Point"), + ("custom", "Custom"), + ], + max_length=16, + ), + ), + ("layer_name", models.CharField(db_index=True, max_length=255)), + ( + "spatial_resolution_in_meters", + models.FloatField(blank=True, null=True), + ), + ( + "ee_layer_name", + models.CharField(blank=True, default="", max_length=255), + ), + ("db_dataset_name", models.CharField(db_index=True, max_length=255)), + ( + "geoserver_workspace_name", + models.CharField(blank=True, default="", max_length=255), + ), + ( + "geoserver_layer_name", + models.CharField(blank=True, default="", max_length=511), + ), + ( + "start_year", + models.CharField(blank=True, default="", max_length=16), + ), + ("end_year", models.CharField(blank=True, default="", max_length=16)), + ( + "style_file_url", + models.CharField(blank=True, default="", max_length=1024), + ), + ("theme", models.CharField(blank=True, default="", max_length=255)), + ("auto_stac", models.BooleanField(default=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Layer Mapping", + "verbose_name_plural": "Layer Mappings", + "unique_together": {("layer_name", "layer_type", "ee_layer_name")}, + }, + ), + ] diff --git a/computing/misc/admin_boundary.py b/computing/misc/admin_boundary.py index 0cd5c080..7f74f87e 100644 --- a/computing/misc/admin_boundary.py +++ b/computing/misc/admin_boundary.py @@ -8,6 +8,7 @@ generate_shape_files, push_shape_to_geoserver, ) +from computing.base_layer_setup import with_base_layers from utilities.gee_utils import ( ee_initialize, valid_gee_text, @@ -26,6 +27,7 @@ @app.task(bind=True) +@with_base_layers("soi_tehsil", "admin_boundary") def generate_tehsil_shape_file_data(self, state, district, block, gee_account_id): """ It will generate Admin boundary of given location as tehsil levels @@ -92,6 +94,7 @@ def create_shp_files(collection, state_dir, district, block, layer_id): return path +@with_base_layers("soi_tehsil", "admin_boundary") def clip_block_from_admin_boundary(state, district, block): census_2011 = None try: diff --git a/computing/misc/agroecological_space_local_compute.py b/computing/misc/agroecological_space_local_compute.py new file mode 100644 index 00000000..d6a8fc2f --- /dev/null +++ b/computing/misc/agroecological_space_local_compute.py @@ -0,0 +1,147 @@ +import os +import geopandas as gpd + +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) +from computing.local_compute_helper import ( + PROJECT_ROOT, + build_output_vector_path, + load_precomputed_watersheds, + read_validated_vector_file, + write_vector_output, + validate_geometry, +) +from computing.config_loader import ( + PAN_INDIA_AGROECOLOGICAL_PATH, + LOCAL_AGROECOLOGICAL_OUTPUT, +) + +GEOSERVER_WORKSPACE = "agroecological" + + +def _compute_agroecological_for_watersheds(watersheds_gdf, agro_gdf): + """ + Spatially joins Agroecological features with watershed polygons. + Equivalent to the GEE Join.saveFirst() with spatial intersection. + """ + if agro_gdf.empty: + return agro_gdf + + if watersheds_gdf.crs and agro_gdf.crs and watersheds_gdf.crs != agro_gdf.crs: + agro_gdf = agro_gdf.to_crs(watersheds_gdf.crs) + + # We only need the 'uid' from watersheds + target_watersheds = watersheds_gdf[["uid", "geometry"]].copy() + + joined_gdf = gpd.sjoin( + agro_gdf, + target_watersheds, + how="inner", + predicate="intersects" + ) + + # To mimic ee.Join.saveFirst(), drop duplicates based on the original feature index + joined_gdf = joined_gdf[~joined_gdf.index.duplicated(keep="first")] + + if "index_right" in joined_gdf.columns: + joined_gdf = joined_gdf.drop(columns=["index_right"]) + + return joined_gdf + + +@app.task(bind=True) +def generate_agroecological_data_local( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + gee_account_id=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if state and district and block: + layer_name = f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_agroecological" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + if not roi_path or not asset_suffix: + raise ValueError("ROI path and asset_suffix are required for custom runs.") + layer_name = f"{valid_gee_text(asset_suffix).lower()}_agroecological" + watersheds_gdf = read_validated_vector_file(roi_path, f"Invalid ROI file: {roi_path}") + print(f"ROI source: {roi_path}") + + if not os.path.exists(PAN_INDIA_AGROECOLOGICAL_PATH): + raise FileNotFoundError(f"PAN INDIA Agroecological file not found at {PAN_INDIA_AGROECOLOGICAL_PATH}") + + print("Loading Agroecological data overlapping ROI...") + agro_gdf = gpd.read_file(PAN_INDIA_AGROECOLOGICAL_PATH, mask=watersheds_gdf) + agro_gdf = validate_geometry(agro_gdf) + if agro_gdf.empty: + raise ValueError("PAN INDIA Agroecological file has no valid geometries overlapping ROI") + print(f"Loaded {len(agro_gdf)} Agroecological features") + + result_gdf = _compute_agroecological_for_watersheds( + watersheds_gdf=watersheds_gdf, + agro_gdf=agro_gdf, + ) + print(f"Final valid Agroecological features after spatial join: {len(result_gdf)}") + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_AGROECOLOGICAL_OUTPUT, + ) + + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local Agroecological vector: {asset_id}") + + layer_at_geoserver = False + + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer response: {geoserver_response}") + if geoserver_response and geoserver_response.get("status_code") in (200, 201): + layer_at_geoserver = True + + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Agroecological", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for Agroecological vector") + + return layer_at_geoserver + + + diff --git a/computing/misc/antyodaya/pipeline.py b/computing/misc/antyodaya/pipeline.py index 308eb214..bff2560b 100644 --- a/computing/misc/antyodaya/pipeline.py +++ b/computing/misc/antyodaya/pipeline.py @@ -13,6 +13,7 @@ import pandas as pd from django.conf import settings +from computing.utils import save_layer_info_to_db, update_layer_sync_status from utilities.pipelines import AdminScope, CSAdminSource, StandardRequest, load_config from utilities.pipelines.admin import ( ADMIN_COLUMN_DESCRIPTIONS, @@ -24,11 +25,12 @@ column_dictionary, frame_profile, input_signatures, + resolved_scope_output_identity, slug, stable_hash, utc_now_text, ) -from utilities.pipelines.publish import publish_gpkg_layer, register_layer +from utilities.pipelines.publish import publish_gpkg_layer from utilities.pipelines.schema import ( STATUS_MATCHED, STATUS_NO_DATA, @@ -49,11 +51,11 @@ ANTYODAYA_2020_CSV, ANTYODAYA_GEOSERVER_WORKSPACE, ) - +from utilities.pipelines import api_request_payload CONFIG_PATH = Path(__file__).with_name("antyodaya_pipeline.yaml") ALGORITHM = "local-antyodaya-csv-admin-join" -ALGORITHM_VERSION = "2.0" +ALGORITHM_VERSION = "2.1" SOURCE_DEFAULTS = { "admin_gpkg": ADMIN_BOUNDARY_GPKG, "csv": ANTYODAYA_2020_CSV, @@ -82,11 +84,9 @@ def _apply_source_defaults(config: Mapping[str, Any]) -> dict[str, Any]: return resolved -def _layer_name(prefix: str, district: str | None, tehsil: str | None) -> str: - return f"{prefix}_{slug(district)}_{slug(tehsil)}".strip("_") - - -def _cli_request(state: str, district: str, tehsil: str, sync_to_geoserver: bool = True) -> StandardRequest: +def _cli_request( + state: str, district: str, tehsil: str, sync_to_geoserver: bool = True +) -> StandardRequest: return StandardRequest.from_mapping( { "scope": { @@ -104,11 +104,19 @@ def _cli_request(state: str, district: str, tehsil: str, sync_to_geoserver: bool def _source_columns(config: Mapping[str, Any]) -> dict[str, list[str]]: header = csv_header(_repo_path(config["sources"]["csv"])) validation = config["validation"] - category_cluster = [col for col in header if col.endswith(validation["category_cluster_suffix"])] - category_value = [col for col in header if col.endswith(validation["category_value_suffix"])] - feature_value = [col for col in header if col.endswith(validation["feature_value_suffix"])] + category_cluster = [ + col for col in header if col.endswith(validation["category_cluster_suffix"]) + ] + category_value = [ + col for col in header if col.endswith(validation["category_value_suffix"]) + ] + feature_value = [ + col for col in header if col.endswith(validation["feature_value_suffix"]) + ] metric_columns = set(category_cluster + category_value + feature_value) - location_columns = [col for col in config["source_location_columns"] if col in header] + location_columns = [ + col for col in config["source_location_columns"] if col in header + ] source_identity_columns = { config["keys"]["source_join_key"], config["keys"]["source_unique_key"], @@ -125,7 +133,9 @@ def _source_columns(config: Mapping[str, Any]) -> dict[str, list[str]]: } -def _sidecar(config: Mapping[str, Any], columns: Mapping[str, list[str]]) -> CSVSQLiteSidecar: +def _sidecar( + config: Mapping[str, Any], columns: Mapping[str, list[str]] +) -> CSVSQLiteSidecar: source_columns = [ config["keys"]["source_join_key"], config["keys"]["source_unique_key"], @@ -146,10 +156,14 @@ def _sidecar(config: Mapping[str, Any], columns: Mapping[str, list[str]]) -> CSV def _normalize_category_clusters(frame: pd.DataFrame, columns: list[str]) -> None: for column in columns: if column in frame.columns: - frame[column] = frame[column].where(frame[column].isna(), frame[column].astype(str).str.upper()) + frame[column] = frame[column].where( + frame[column].isna(), frame[column].astype(str).str.upper() + ) -def _validate_antyodaya(frame: pd.DataFrame, config: Mapping[str, Any]) -> list[ValidationIssue]: +def _validate_antyodaya( + frame: pd.DataFrame, config: Mapping[str, Any] +) -> list[ValidationIssue]: validation = config["validation"] category_columns = columns_ending_with(frame, validation["category_cluster_suffix"]) value_columns = columns_ending_with(frame, validation["category_value_suffix"]) @@ -175,7 +189,9 @@ def _validate_antyodaya(frame: pd.DataFrame, config: Mapping[str, Any]) -> list[ return issues -def _merge_admin_antyodaya(admin_rows, source_rows: pd.DataFrame, config: Mapping[str, Any]): +def _merge_admin_antyodaya( + admin_rows, source_rows: pd.DataFrame, config: Mapping[str, Any] +): admin_key = config["keys"]["admin_join_key"] source_key = config["keys"]["source_join_key"] admin = admin_rows.copy() @@ -185,17 +201,36 @@ def _merge_admin_antyodaya(admin_rows, source_rows: pd.DataFrame, config: Mappin "district_name", "sub_district_name", } - attrs = attrs.drop(columns=[col for col in source_duplicate_columns if col in attrs.columns], errors="ignore") - return admin.merge(attrs, left_on=admin_key, right_on=source_key, how="left", suffixes=("", "_antyodaya")) + attrs = attrs.drop( + columns=[col for col in source_duplicate_columns if col in attrs.columns], + errors="ignore", + ) + return admin.merge( + attrs, + left_on=admin_key, + right_on=source_key, + how="left", + suffixes=("", "_antyodaya"), + ) -def _ordered_tabular_columns(frame: pd.DataFrame, columns: Mapping[str, list[str]]) -> list[str]: +def _ordered_tabular_columns( + frame: pd.DataFrame, columns: Mapping[str, list[str]] +) -> list[str]: ordered = [] - ordered.extend([col for col in columns["location"] if col not in ordered and col in frame.columns]) + ordered.extend( + [ + col + for col in columns["location"] + if col not in ordered and col in frame.columns + ] + ) ordered.extend([col for col in columns["category_cluster"] if col in frame.columns]) ordered.extend([col for col in columns["category_value"] if col in frame.columns]) ordered.extend([col for col in columns["feature_value"] if col in frame.columns]) - ordered.extend([col for col in columns["raw"] if col in frame.columns and col not in ordered]) + ordered.extend( + [col for col in columns["raw"] if col in frame.columns and col not in ordered] + ) return [col for col in ordered if col in frame.columns] @@ -210,17 +245,27 @@ def _report_value_columns(columns: Mapping[str, list[str]]) -> list[str]: ] -def _focused_frame(frame: pd.DataFrame, columns: Mapping[str, list[str]], status_name: str | None) -> pd.DataFrame: - focused = admin_presentation_frame(frame.drop(columns=["geometry"], errors="ignore")) +def _focused_frame( + frame: pd.DataFrame, columns: Mapping[str, list[str]], status_name: str | None +) -> pd.DataFrame: + focused = admin_presentation_frame( + frame.drop(columns=["geometry"], errors="ignore") + ) source = frame.set_index("fid", drop=False) if "fid" in frame.columns else frame metric_columns = _report_value_columns(columns) output_rows: list[dict[str, Any]] = [] for _, admin_row in focused.iterrows(): row = admin_row.to_dict() admin_index = row.get("index") - values = source.loc[admin_index] if admin_index in source.index else pd.Series(dtype=object) + values = ( + source.loc[admin_index] + if admin_index in source.index + else pd.Series(dtype=object) + ) has_village_id = pd.notna(row.get("village_id")) - has_antyodaya = pd.notna(values.get("village_key")) if not values.empty else False + has_antyodaya = ( + pd.notna(values.get("village_key")) if not values.empty else False + ) status = STATUS_MATCHED if not has_village_id: status = STATUS_NO_VILLAGE_ID @@ -237,7 +282,11 @@ def _focused_frame(frame: pd.DataFrame, columns: Mapping[str, list[str]], status ordered = list(focused.columns) if status_name: ordered.append(status_name) - ordered.extend(column for column in metric_columns if column in output.columns and column not in ordered) + ordered.extend( + column + for column in metric_columns + if column in output.columns and column not in ordered + ) return output.reindex(columns=ordered) @@ -314,7 +363,9 @@ def describe(name: str) -> str | None: return describe -def _overview(frame: pd.DataFrame, group_columns: list[str], columns: Mapping[str, list[str]]) -> pd.DataFrame: +def _overview( + frame: pd.DataFrame, group_columns: list[str], columns: Mapping[str, list[str]] +) -> pd.DataFrame: available_group_columns = [col for col in group_columns if col in frame.columns] if not available_group_columns: return pd.DataFrame() @@ -324,14 +375,20 @@ def _overview(frame: pd.DataFrame, group_columns: list[str], columns: Mapping[st keys = (keys,) row = dict(zip(available_group_columns, keys)) row["admin_village_rows"] = int(len(group)) - row["matched_antyodaya_rows"] = int(group["village_key"].notna().sum()) if "village_key" in group else 0 + row["matched_antyodaya_rows"] = ( + int(group["village_key"].notna().sum()) if "village_key" in group else 0 + ) for column in columns["category_value"]: if column in group.columns: - row[f"{column}_mean"] = float(pd.to_numeric(group[column], errors="coerce").mean()) + row[f"{column}_mean"] = float( + pd.to_numeric(group[column], errors="coerce").mean() + ) for column in columns["category_cluster"]: if column not in group.columns: continue - counts = group[column].fillna("NO_DATA").astype(str).str.upper().value_counts() + counts = ( + group[column].fillna("NO_DATA").astype(str).str.upper().value_counts() + ) for label in ("HIGH", "MEDIUM", "LOW", "NO_DATA"): row[f"{column}_{label.lower()}_count"] = int(counts.get(label, 0)) rows.append(row) @@ -346,8 +403,12 @@ def _column_reference_lines(column_entries: list[Mapping[str, Any]]) -> list[str "| --- | --- | --- |", ] for entry in column_entries: - description = str(entry.get("description") or "").replace("|", "\\|").replace("\n", " ") - lines.append(f"| `{entry['column']}` | {entry.get('datatype', '')} | {description} |") + description = ( + str(entry.get("description") or "").replace("|", "\\|").replace("\n", " ") + ) + lines.append( + f"| `{entry['column']}` | {entry.get('datatype', '')} | {description} |" + ) lines.append("") return lines @@ -420,7 +481,9 @@ def _readme_lines( return lines -def _cache_input_signatures(config: Mapping[str, Any], config_path: str | Path) -> dict[str, dict[str, Any]]: +def _cache_input_signatures( + config: Mapping[str, Any], config_path: str | Path +) -> dict[str, dict[str, Any]]: sources = config.get("sources", {}) paths: dict[str, str | Path] = { "pipeline_config": _repo_path(config_path), @@ -448,7 +511,9 @@ def _cache_key(request: StandardRequest, outputs: OutputOptions) -> str: ) -def _required_result_paths(outputs: OutputOptions, request: StandardRequest) -> tuple[str, ...]: +def _required_result_paths( + outputs: OutputOptions, request: StandardRequest +) -> tuple[str, ...]: required: list[str] = ["mapping_yaml_path", "links_path"] if outputs.metadata: required.append("run_metadata_path") @@ -472,9 +537,27 @@ def run_antyodaya_pipeline( outputs = resolve_output_options(request, config) columns = _source_columns(config) output_config = config["output"] - layer_name = _layer_name(output_config["layer_prefix"], request.scope.district_name, request.scope.tehsil_name) - result_name = layer_name or f"{output_config['layer_prefix']}_{slug(request.scope.level)}" - output_root = _repo_path(output_config["root"]) / slug(request.scope.state_name) / slug(request.scope.district_name) / slug(request.scope.tehsil_name) + + t0 = time.perf_counter() + admin_source = CSAdminSource( + _repo_path(config["sources"]["admin_gpkg"]), + table_name=config["sources"]["admin_layer"], + ) + include_geometry = outputs.gpkg or request.publish.sync_to_geoserver + ( + admin_selection, + registration_scope, + output_parts, + result_name, + ) = resolved_scope_output_identity( + admin_source, + output_config["layer_prefix"], + AdminScope.from_mapping(asdict(request.scope)), + include_geometry=include_geometry, + ) + timings["read_admin_seconds"] = round(time.perf_counter() - t0, 3) + + output_root = _repo_path(output_config["root"]).joinpath(*output_parts) bundle = OutputBundle( output_root, result_name, @@ -493,12 +576,6 @@ def run_antyodaya_pipeline( cached["cache_hit"] = True return cached - t0 = time.perf_counter() - admin_source = CSAdminSource(_repo_path(config["sources"]["admin_gpkg"]), table_name=config["sources"]["admin_layer"]) - include_geometry = outputs.gpkg or request.publish.sync_to_geoserver - admin_selection = admin_source.read_scope(AdminScope.from_mapping(asdict(request.scope)), include_geometry=include_geometry) - timings["read_admin_seconds"] = round(time.perf_counter() - t0, 3) - t0 = time.perf_counter() sidecar = _sidecar(config, columns) sidecar_status = sidecar.materialize() @@ -521,17 +598,24 @@ def run_antyodaya_pipeline( status_name, status_outputs = status_column_config(config) if status_name: joined[status_name] = [ - STATUS_NO_VILLAGE_ID - if pd.isna(village_id) - else (STATUS_MATCHED if pd.notna(village_key) else STATUS_NO_DATA) + ( + STATUS_NO_VILLAGE_ID + if pd.isna(village_id) + else (STATUS_MATCHED if pd.notna(village_key) else STATUS_NO_DATA) + ) for village_id, village_key in zip(joined["village_id"], village_keys) ] ordered_columns = _ordered_tabular_columns(joined, columns) - villages_frame = admin_output_frame(joined.drop(columns=["geometry"], errors="ignore"), value_columns=ordered_columns) + villages_frame = admin_output_frame( + joined.drop(columns=["geometry"], errors="ignore"), + value_columns=ordered_columns, + ) gpkg_value_columns = list(ordered_columns) if status_name and {"gpkg", "geoserver"} & status_outputs: gpkg_value_columns = [status_name, *gpkg_value_columns] - gpkg_frame = admin_output_frame(joined, value_columns=gpkg_value_columns, include_geometry=True) + gpkg_frame = admin_output_frame( + joined, value_columns=gpkg_value_columns, include_geometry=True + ) gpkg_frame = normalize_unicode_frame(gpkg_frame) describe = _column_describer(config, columns) timings["build_outputs_seconds"] = round(time.perf_counter() - t0, 3) @@ -556,7 +640,9 @@ def run_antyodaya_pipeline( "layer_name": result_name, "rows": int(len(villages_frame)), "matched_rows": matched_rows, - "join_coverage": round(matched_rows / len(villages_frame), 6) if len(villages_frame) else 0, + "join_coverage": ( + round(matched_rows / len(villages_frame), 6) if len(villages_frame) else 0 + ), "validation_issues": [asdict(issue) for issue in validation_issues], "sidecar": sidecar_status, "admin_created_indexes": admin_selection.created_indexes, @@ -597,29 +683,6 @@ def run_antyodaya_pipeline( geoserver = asdict(geoserver_result) geoserver["ok"] = True geoserver["status"] = "published" - if request.publish.register_layers: - result["layer_registration"] = register_layer( - dataset_name=output_config.get("dataset_name", "Antyodaya 2020"), - layer_name=result_name, - scope=request.scope, - workspace=geoserver_workspace, - geoserver_url=geoserver.get("wfs_url"), - algorithm=ALGORITHM, - algorithm_version=ALGORITHM_VERSION, - misc={ - "source_csv": config["sources"]["csv"], - "gpkg_path": result.get("gpkg_path"), - "links_path": result.get("links_path"), - "output_dir": bundle.path.as_posix(), - "geoserver_layer_name": result_name, - "geoserver_url": geoserver.get("wfs_url"), - "rows": result.get("rows"), - "matched_rows": result.get("matched_rows"), - "join_coverage": result.get("join_coverage"), - }, - overwrite=request.publish.overwrite, - ) - result["layer_id"] = (result["layer_registration"] or {}).get("layer_id") except Exception as exc: geoserver = { "ok": False, @@ -631,6 +694,38 @@ def run_antyodaya_pipeline( } timings["publish_geoserver_seconds"] = round(time.perf_counter() - t0, 3) result["geoserver"] = geoserver + if request.publish.register_layers and geoserver and geoserver.get("ok"): + state = registration_scope.state_name + district = registration_scope.district_name + block = registration_scope.tehsil_name + dataset_name = output_config.get("dataset_name", "Antyodaya 2020") + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=result_name, + asset_id="not applicable: local compute GeoServer layer", + dataset_name=dataset_name, + algorithm=ALGORITHM, + algorithm_version=ALGORITHM_VERSION, + misc={"is_generated_locally": True}, + is_override=request.publish.overwrite, + ) + if layer_id is None: + raise RuntimeError( + f"Database registration failed for layer {result_name!r}." + ) + if update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) is None: + raise RuntimeError( + f"GeoServer sync status update failed for layer ID {layer_id}." + ) + result["layer_id"] = layer_id + result["layer_registration"] = { + "ok": True, + "status": "registered", + "dataset": dataset_name, + "layer_id": layer_id, + } if outputs.readme: result["readme_path"] = bundle.write_readme( _readme_lines( @@ -642,7 +737,9 @@ def run_antyodaya_pipeline( issues=validation_issues, geoserver=geoserver, column_entries=column_dictionary( - pd.DataFrame(gpkg_frame.drop(columns=["geometry"], errors="ignore")), + pd.DataFrame( + gpkg_frame.drop(columns=["geometry"], errors="ignore") + ), describe, ), ) @@ -670,7 +767,9 @@ def run_antyodaya_pipeline( "config_path": str(config_path), "outputs": { "villages": frame_profile( - pd.DataFrame(gpkg_frame.drop(columns=["geometry"], errors="ignore")), + pd.DataFrame( + gpkg_frame.drop(columns=["geometry"], errors="ignore") + ), describe, ), }, @@ -694,14 +793,27 @@ def run_antyodaya_request(payload: Mapping[str, Any]) -> dict[str, Any]: @app.task(bind=True) -def generate_antyodaya_layer_task(self, payload: Mapping[str, Any]) -> dict[str, Any]: +def generate_antyodaya_layer_task( + self, + state: str | None = None, + district: str | None = None, + block: str | None = None, + payload: Mapping[str, Any] | None = None, +) -> dict[str, Any]: """Generate Mission Antyodaya outputs for a standard request payload.""" + if payload is None: + payload = api_request_payload( + {"state": state, "district": district, "block": block}, + overwrite=True, + ) return run_antyodaya_request(payload) def main() -> None: - parser = argparse.ArgumentParser(description="Run the local Mission Antyodaya pipeline.") + parser = argparse.ArgumentParser( + description="Run the local Mission Antyodaya pipeline." + ) parser.add_argument("--state") parser.add_argument("--district") parser.add_argument("--tehsil") diff --git a/computing/misc/antyodaya_local_compute.py b/computing/misc/antyodaya_local_compute.py new file mode 100644 index 00000000..0b8b3dce --- /dev/null +++ b/computing/misc/antyodaya_local_compute.py @@ -0,0 +1,147 @@ +"""Mission Antyodaya tehsil clipping from the pan-India local GeoPackage. + +Runtime contract: + +- request names may be spaces or snake_case; stored names are resolved from the + GeoPackage before clipping +- local output is always written first +- GeoServer publish is enabled by default and can be disabled per request; + failure is reported without breaking local generation + +The source GeoPackage should have an attribute index on +``(state_name, district_name, TEHSIL)``. The task creates it once if missing, +which keeps reads sub-second for normal tehsil clips on the local server. +""" + +import os +import geopandas as gpd + +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) +from computing.local_compute_helper import ( + PROJECT_ROOT, + build_output_vector_path, + load_precomputed_panchayat, + read_validated_vector_file, + write_vector_output, + validate_geometry, +) +from computing.config_loader import ( + PAN_INDIA_ANTYODAYA_2020, + LOCAL_ANTYODAYA_2020_OUTPUT, +) + +GEOSERVER_WORKSPACE = "antyodaya_2020" + + +def _compute_antyodaya_for_panchayat(panchayat_gdf, antyodaya_gdf): + if antyodaya_gdf.empty: + return antyodaya_gdf + + outer_boundary = panchayat_gdf.geometry.unary_union + + # Clip Antyodaya geometries to the panchayat boundary + antyodaya_in_roi = gpd.clip(antyodaya_gdf, outer_boundary).copy() + + # Final cleanup + antyodaya_in_roi = antyodaya_in_roi[~antyodaya_in_roi.geometry.is_empty] + antyodaya_in_roi = antyodaya_in_roi[antyodaya_in_roi.geometry.is_valid] + antyodaya_in_roi = antyodaya_in_roi[antyodaya_in_roi.geometry.notna()] + + return antyodaya_in_roi + + +@app.task(bind=True) +def generate_antyodaya_data_local( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + gee_account_id=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if state and district and block: + layer_name = f"antyodaya20_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}" + panchayat_gdf, watershed_source = load_precomputed_panchayat( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + if not roi_path or not asset_suffix: + raise ValueError("ROI path and asset_suffix are required for custom runs.") + layer_name = f"antyodaya20_{valid_gee_text(asset_suffix).lower()}" + panchayat_gdf = read_validated_vector_file(roi_path, f"Invalid ROI file: {roi_path}") + print(f"ROI source: {roi_path}") + + if not os.path.exists(PAN_INDIA_ANTYODAYA_2020): + raise FileNotFoundError(f"PAN INDIA Antyodaya file not found at {PAN_INDIA_ANTYODAYA_2020}") + + print("Loading Antyodaya data overlapping ROI...") + antyodaya_gdf = gpd.read_file(PAN_INDIA_ANTYODAYA_2020, mask=panchayat_gdf) + antyodaya_gdf = validate_geometry(antyodaya_gdf) + if antyodaya_gdf.empty: + print("Warning: PAN INDIA Antyodaya file has no valid geometries overlapping ROI") + else: + print(f"Loaded {len(antyodaya_gdf)} Antyodaya features") + + result_gdf = _compute_antyodaya_for_panchayat( + panchayat_gdf=panchayat_gdf, + antyodaya_gdf=antyodaya_gdf, + ) + print(f"Final valid Antyodaya features after spatial filter: {len(result_gdf)}") + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_ANTYODAYA_2020_OUTPUT, + ) + + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local Antyodaya vector: {asset_id}") + + layer_at_geoserver = False + + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer response: {geoserver_response}") + if geoserver_response and geoserver_response.get("status_code") in (200, 201): + layer_at_geoserver = True + + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Antyodaya 2020", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for Antyodaya vector") + + return layer_at_geoserver \ No newline at end of file diff --git a/computing/misc/aquifer_vector_local.py b/computing/misc/aquifer_vector_local.py new file mode 100644 index 00000000..c67a4281 --- /dev/null +++ b/computing/misc/aquifer_vector_local.py @@ -0,0 +1,419 @@ +import os + +import pandas as pd +from utilities.gee_utils import valid_gee_text + +from nrm_app.celery import app + +from computing.config_loader import ( + AQUIFER_VECTOR_PATH, + AQUIFER_VECTOR_OUTPUT_DIR as LOCAL_OUTPUT_BASE_DIR, +) +from computing.local_compute_helper import ( + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + build_output_vector_path, + get_watershed_areas_in_hectares, + load_precomputed_watersheds, + read_validated_vector_file, + validate_geometry, + write_vector_output, +) +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) +GEOSERVER_WORKSPACE = "aquifer" + +YIELD_VALUE_MAP = { + "": None, + "-": None, + "Upto 2%": 0.02, + "1-2%": 0.02, + "Upto 1.5%": 0.015, + "Upto 3%": 0.03, + "Upto 2.5%": 0.025, + "6 - 8%": 0.08, + "1-1.5%": 0.015, + "2-3%": 0.03, + "Upto 4%": 0.04, + "Upto 5%": 0.05, + "Upto -3.5%": 0.035, + "Upto 3 %": 0.03, + "Upto 9%": 0.09, + "1-2.5": 0.025, + "Upto 1.2%": 0.012, + "Upto 5-2%": 0.05, + "Upto 1%": 0.01, + "Up to 1.5%": 0.015, + "Upto 8%": 0.08, + "Upto 6%": 0.06, + "0.08": 0.08, + "8 - 16%": 0.16, + "Not Explored": None, + "8 - 15%": 0.15, + "6 - 10%": 0.10, + "6 - 15%": 0.15, + "8 - 20%": 0.20, + "8 - 10%": 0.10, + "6 - 12%": 0.12, + "6 - 16%": 0.16, + "8 - 12%": 0.12, + "8 - 18%": 0.18, + "Upto 3.5%": 0.035, + "Upto 15%": 0.15, + "1.5-2%": 0.02, +} + +PRINCIPAL_AQUIFERS = [ + "Laterite", + "Basalt", + "Sandstone", + "Shale", + "Limestone", + "Granite", + "Schist", + "Quartzite", + "Charnockite", + "Khondalite", + "Banded Gneissic Complex", + "Gneiss", + "Intrusive", + "Alluvium", + "None", +] + +PRINCIPAL_AQUIFER_PERCENT_COLUMNS = [ + f"principle_aq_{aquifer_name}_percent" + for aquifer_name in PRINCIPAL_AQUIFERS +] + + +def _safe_string(value): + if pd.isna(value): + return "" + return str(value).strip() + + +def _safe_int(value): + if pd.isna(value): + return None + return int(float(value)) + + +def _normalize_principal_name(value): + principal_name = _safe_string(value) + return principal_name or "None" + + +def _map_yield_value(value): + return YIELD_VALUE_MAP.get(_safe_string(value)) + + +def _build_empty_aquifer_properties(watershed_row, area_in_ha): + properties = { + "uid": watershed_row.get("uid"), + "id": watershed_row.get("id", watershed_row.get("uid")), + "area_in_ha": float(area_in_ha) if pd.notna(area_in_ha) else 0.0, + "total_weighted_yield": 0.0, + "%_area_aquifer": 0.0, + "aquifer_count": 0, + "aquifer_class": "No Data", + "Age": "", + "Lithology_": None, + "Major_Aq_1": "", + "Major_Aqui": "", + "Principal_": "", + "Recommende": None, + "yeild__": "", + "zone_m": "", + "y_value": None, + } + properties.update( + { + column_name: 0.0 + for column_name in PRINCIPAL_AQUIFER_PERCENT_COLUMNS + } + ) + return properties + + +def _build_aquifer_properties(watershed_row, area_in_ha, intersections_df): + properties = _build_empty_aquifer_properties(watershed_row, area_in_ha) + dominant_aquifer = intersections_df.sort_values( + "intersection_area_m2", + ascending=False, + ).iloc[0] + pct_by_aquifer = intersections_df.groupby("principal_name")[ + "percent_area_aquifer" + ].sum() + + for aquifer_name in PRINCIPAL_AQUIFERS: + properties[f"principle_aq_{aquifer_name}_percent"] = float( + pct_by_aquifer.get(aquifer_name, 0.0) + ) + + principal_value = _safe_string(dominant_aquifer["Principal_"]) + properties.update( + { + "total_weighted_yield": float( + intersections_df["weighted_contribution"].sum() + ), + "%_area_aquifer": float(dominant_aquifer["percent_area_aquifer"]), + "aquifer_count": int(len(intersections_df)), + "aquifer_class": ( + "Alluvium" + if principal_value == "Alluvium" + else "Hard-Rock" + ), + "Age": _safe_string(dominant_aquifer["Age"]), + "Lithology_": _safe_int(dominant_aquifer["Lithology_"]), + "Major_Aq_1": _safe_string(dominant_aquifer["Major_Aq_1"]), + "Major_Aqui": _safe_string(dominant_aquifer["Major_Aqui"]), + "Principal_": principal_value, + "Recommende": _safe_int(dominant_aquifer["Recommende"]), + "yeild__": _safe_string(dominant_aquifer["yeild__"]), + "zone_m": _safe_string(dominant_aquifer["zone_m"]), + "y_value": ( + float(dominant_aquifer["y_value"]) + if pd.notna(dominant_aquifer["y_value"]) + else None + ), + } + ) + return properties + + +def _compute_aquifer_properties_for_watersheds(watersheds_gdf, aquifers_gdf): + watersheds_gdf = validate_geometry(watersheds_gdf) + if watersheds_gdf.empty: + raise ValueError("No valid watershed geometries found for local processing.") + if watersheds_gdf.crs is None: + raise ValueError("Watershed CRS is missing; cannot compute aquifer overlaps.") + + aquifers_gdf = validate_geometry(aquifers_gdf) + if aquifers_gdf.empty: + raise ValueError("Aquifer source file has no valid geometries.") + if aquifers_gdf.crs is None: + raise ValueError("Aquifer source CRS is missing; cannot compute overlaps.") + + watersheds_result = watersheds_gdf.copy() + watersheds_result["area_in_ha"] = get_watershed_areas_in_hectares( + watersheds_result + ).astype(float) + + aquifers_with_yield = aquifers_gdf.copy() + aquifers_with_yield["y_value"] = aquifers_with_yield["yeild__"].apply( + _map_yield_value + ) + aquifers_with_yield = aquifers_with_yield.loc[ + aquifers_with_yield["y_value"].notna() + ].copy() + if aquifers_with_yield.empty: + raise ValueError("Aquifer source has no records with valid yield values.") + + watersheds_projected = watersheds_result.to_crs("EPSG:6933") + aquifers_projected = aquifers_with_yield.to_crs("EPSG:6933") + + computed_rows = [] + total = len(watersheds_projected) + + for index, watershed_idx in enumerate(watersheds_projected.index, start=1): + watershed_geometry = watersheds_projected.at[watershed_idx, "geometry"] + watershed_row = watersheds_result.loc[watershed_idx] + area_in_ha = watersheds_result.at[watershed_idx, "area_in_ha"] + + if watershed_geometry is None or watershed_geometry.is_empty: + computed_rows.append( + _build_empty_aquifer_properties(watershed_row, area_in_ha) + ) + continue + + watershed_area_m2 = float(watershed_geometry.area) + if watershed_area_m2 <= 0: + computed_rows.append( + _build_empty_aquifer_properties(watershed_row, area_in_ha) + ) + continue + + intersecting_aquifers = aquifers_projected.loc[ + aquifers_projected.intersects(watershed_geometry) + ] + + intersections = [] + for _, aquifer_row in intersecting_aquifers.iterrows(): + intersection_geometry = watershed_geometry.intersection( + aquifer_row.geometry + ) + if intersection_geometry.is_empty: + continue + + intersection_area_m2 = float(intersection_geometry.area) + if intersection_area_m2 <= 0: + continue + + fraction = intersection_area_m2 / watershed_area_m2 + intersections.append( + { + "intersection_area_m2": intersection_area_m2, + "percent_area_aquifer": fraction * 100.0, + "weighted_contribution": fraction * float(aquifer_row["y_value"]), + "principal_name": _normalize_principal_name( + aquifer_row["Principal_"] + ), + "Age": aquifer_row["Age"], + "Lithology_": aquifer_row["Lithology_"], + "Major_Aq_1": aquifer_row["Major_Aq_1"], + "Major_Aqui": aquifer_row["Major_Aqui"], + "Principal_": aquifer_row["Principal_"], + "Recommende": aquifer_row["Recommende"], + "yeild__": aquifer_row["yeild__"], + "zone_m": aquifer_row["zone_m"], + "y_value": aquifer_row["y_value"], + } + ) + + if intersections: + intersections_df = pd.DataFrame(intersections) + computed_rows.append( + _build_aquifer_properties( + watershed_row, + area_in_ha, + intersections_df, + ) + ) + else: + computed_rows.append( + _build_empty_aquifer_properties(watershed_row, area_in_ha) + ) + + if index % 200 == 0 or index == total: + print(f"Computed aquifer properties for {index}/{total} watersheds") + + computed_df = pd.DataFrame(computed_rows) + for column in computed_df.columns: + watersheds_result[column] = computed_df[column].values + return watersheds_result + + +def run_aquifer_vector_local( + state, + district, + block, + aquifer_vector_path=AQUIFER_VECTOR_PATH, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + push_to_geoserver=True, + sync_layer_metadata=True, +): + state = str(state).strip().lower() + district = str(district).strip().lower() + block = str(block).strip().lower() + + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + aquifers_gdf = read_validated_vector_file( + aquifer_vector_path, + f"Aquifer source file has no valid geometries: {aquifer_vector_path}", + ) + + result_gdf = _compute_aquifer_properties_for_watersheds( + watersheds_gdf=watersheds_gdf, + aquifers_gdf=aquifers_gdf, + ) + + layer_name = ( + f"aquifer_vector_{valid_gee_text(district.lower())}_" + f"{valid_gee_text(block.lower())}" + ) + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + block_fallback="unknown_block", + ) + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local aquifer vector: {asset_id}") + print(f"Watershed boundary source: {watershed_source}") + + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer response: {geoserver_response}") + if not isinstance(geoserver_response, dict) or geoserver_response.get( + "status_code" + ) not in (200, 201): + return False + + if sync_layer_metadata: + from computing.STAC_specs import generate_STAC_layerwise + + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Aquifer", + misc={"is_generated_locally": True}, + ) + if layer_id and push_to_geoserver: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + layer_stac_generated = generate_STAC_layerwise.generate_vector_stac( + state=state, + district=district, + block=block, + layer_name="aquifer_vector", + ) + update_layer_sync_status( + layer_id=layer_id, + is_stac_specs_generated=layer_stac_generated, + ) + + return True + + +def _generate_aquifer_vector_local_task( + state, + district, + block, + gee_account_id=None, +): + _ = gee_account_id + return run_aquifer_vector_local( + state=state, + district=district, + block=block, + push_to_geoserver=True, + sync_layer_metadata=True, + ) + + +@app.task(bind=True) +def generate_aquifer_vector( + self, + state, + district, + block, + gee_account_id=None, +): + _ = self + return _generate_aquifer_vector_local_task( + state=state, + district=district, + block=block, + gee_account_id=gee_account_id, + ) diff --git a/computing/misc/canal_local_compute.py b/computing/misc/canal_local_compute.py new file mode 100644 index 00000000..41ee2dc3 --- /dev/null +++ b/computing/misc/canal_local_compute.py @@ -0,0 +1,202 @@ +import os +import json +import datetime +import pandas as pd +import geopandas as gpd +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.local_compute_helper import ( + PROJECT_ROOT, + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + build_output_vector_path, + get_watershed_areas_in_hectares, + load_precomputed_watersheds, + read_validated_vector_file, + validate_geometry, + write_vector_output, +) +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, + fix_invalid_geometry_in_gdf, +) + +from computing.config_loader import ( + PAN_INDIA_CANAL_PATH, + LOCAL_CANAL_OUTPUT, +) + +GEOSERVER_WORKSPACE = "canal" + + +def _compute_canal_properties_for_watersheds(watersheds_gdf, canals_gdf): + watersheds_gdf = validate_geometry(watersheds_gdf) + canals_gdf = validate_geometry(canals_gdf) + + watersheds_gdf = watersheds_gdf.reset_index(drop=True) + outer_boundary = watersheds_gdf.geometry.unary_union + + canals_in_roi = canals_gdf[canals_gdf.intersects(outer_boundary)].copy() + + if canals_in_roi.empty: + print("No canals found within the outer boundary.") + return canals_in_roi + + # For each canal, collect every watershed it touches + matched_joined = gpd.sjoin( + canals_in_roi, + watersheds_gdf[["uid", "area_in_ha", "geometry"]], + how="inner", + predicate="intersects", + ) + + #Identify Gap Canals (no watershed match) + matched_indices = matched_joined.index.unique() + gap_canals = canals_in_roi.loc[~canals_in_roi.index.isin(matched_indices)].copy() + + result_segments = [] + + # Expand matched canals → clip to individual watersheds + if not matched_joined.empty: + def clip_matched(row): + watershed_geom = watersheds_gdf.geometry.iloc[row.index_right] + return row.geometry.intersection(watershed_geom) + + matched_joined["geometry"] = matched_joined.apply(clip_matched, axis=1) + # Keep only LineStrings + matched_joined = matched_joined[ + matched_joined.geometry.type.isin(["LineString", "MultiLineString"]) + ] + result_segments.append(matched_joined) + + #Handle gap canals → clip to outer ROI boundary + if not gap_canals.empty: + gap_canals["uid"] = "" + gap_canals["area_in_ha"] = "" + + def clip_gap(row): + return row.geometry.intersection(outer_boundary) + + gap_canals["geometry"] = gap_canals.apply(clip_gap, axis=1) + gap_canals = gap_canals[ + gap_canals.geometry.type.isin(["LineString", "MultiLineString"]) + ] + result_segments.append(gap_canals) + + if not result_segments: + return gpd.GeoDataFrame(columns=canals_gdf.columns, crs=canals_gdf.crs) + + # Merge, Clean and Fix Geometries + final_gdf = gpd.GeoDataFrame(pd.concat(result_segments, ignore_index=True), crs=canals_gdf.crs) + final_gdf["uid"] = final_gdf["uid"].astype(str) + final_gdf["area_in_ha"] = final_gdf["area_in_ha"].astype(str) + + rename_cols = {} + if "st_length(" in final_gdf.columns: + rename_cols["st_length("] = "st_length" + + if rename_cols: + final_gdf = final_gdf.rename(columns=rename_cols) + + final_gdf = final_gdf[~final_gdf.geometry.is_empty] + final_gdf = fix_invalid_geometry_in_gdf(final_gdf) + + if "index_right" in final_gdf.columns: + final_gdf = final_gdf.drop(columns=["index_right"]) + + return final_gdf + + +@app.task(bind=True) +def canal_vector( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi=None, + asset_folder_list=None, + app_type="MWS", + gee_account_id=None, + canal_vector_path=PAN_INDIA_CANAL_PATH, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if state and district and block: + layer_name = f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_canal_vector" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + if not roi or not asset_suffix: + raise ValueError( + "For non state/district/block runs, both `roi` and `asset_suffix` are required." + ) + layer_name = f"{asset_suffix}_canal_vector".lower() + watersheds_gdf = read_validated_vector_file( + roi, + f"ROI file has no valid geometries: {roi}", + ) + print(f"ROI source: {roi}") + + if not os.path.exists(canal_vector_path): + raise FileNotFoundError(f"Canal source file not found: {canal_vector_path}") + + print(f"Loading canal source: {canal_vector_path}") + canals_gdf = read_validated_vector_file( + canal_vector_path, + f"Canal source file has no valid geometries: {canal_vector_path}", + ) + + result_gdf = _compute_canal_properties_for_watersheds( + watersheds_gdf=watersheds_gdf, + canals_gdf=canals_gdf, + ) + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_CANAL_OUTPUT, + ) + + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local canal vector: {asset_id}") + + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer response: {geoserver_response}") + + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Canal Vector", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for canal vector") + + return True + + diff --git a/computing/misc/catchment_area_local_compute.py b/computing/misc/catchment_area_local_compute.py new file mode 100644 index 00000000..67443f97 --- /dev/null +++ b/computing/misc/catchment_area_local_compute.py @@ -0,0 +1,120 @@ +import os +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.utils import ( + save_layer_info_to_db, + update_layer_sync_status, +) +from computing.local_compute_helper import ( + PROJECT_ROOT, + build_output_raster_path, + load_precomputed_watersheds, + read_validated_vector_file, + clip_raster_with_roi, + push_local_raster_to_geoserver, +) +from computing.STAC_specs import generate_STAC_layerwise +from computing.config_loader import ( + PAN_INDIA_CATCHMENT_AREA_PATH, + LOCAL_CATCHMENT_AREA_OUTPUT, +) + +GEOSERVER_WORKSPACE = "catchment_area_singleflow" +CATCHMENT_AREA_STYLE_NAME = "catchment_area_singleflow" + + +@app.task(bind=True) +def generate_catchment_area_singleflow_local( + self, + state=None, + district=None, + block=None, + gee_account_id=None, + proj_id=None, + roi_path=None, + asset_suffix=None, + asset_folder=None, + app_type="MWS", + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if state and district and block: + layer_name_base = f"catchment_area_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_raster" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + if not roi_path or not asset_suffix: + raise ValueError("ROI path and asset_suffix are required for custom runs.") + layer_name_base = f"catchment_area_{asset_suffix}_raster".lower() + watersheds_gdf = read_validated_vector_file( + roi_path, f"Invalid ROI file: {roi_path}" + ) + print(f"ROI source: {roi_path}") + + # Raster Processing + raster_layer_name = f"{layer_name_base}" + output_raster_path = build_output_raster_path( + layer_name=raster_layer_name, + output_base_dir=LOCAL_CATCHMENT_AREA_OUTPUT, + state=state, + district=district, + block=block, + ) + + print("Clipping Catchment Area raster...") + clipped_raster_path = clip_raster_with_roi( + roi_gdf=watersheds_gdf, + raster_path=PAN_INDIA_CATCHMENT_AREA_PATH, + output_path=output_raster_path, + raster_label="Catchment Area Raster", + ) + print(f"Saved clipped Catchment Area raster to: {clipped_raster_path}") + + layer_at_geoserver = False + if push_to_geoserver: + push_local_raster_to_geoserver( + file_path=str(clipped_raster_path), + layer_name=raster_layer_name, + workspace=GEOSERVER_WORKSPACE, + style_name=CATCHMENT_AREA_STYLE_NAME, + ) + print(f"Pushed raster {raster_layer_name} to GeoServer.") + layer_at_geoserver = True + + if sync_layer_metadata and state and district and block: + raster_layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=raster_layer_name, + asset_id=str(clipped_raster_path), + dataset_name="Catchment Area", + misc={"is_generated_locally": True}, + ) + if raster_layer_id: + update_layer_sync_status(layer_id=raster_layer_id, sync_to_geoserver=True) + print(f"Database record updated for raster layer_id: {raster_layer_id}") + + # STAC Specs for Raster + try: + layer_STAC_generated = generate_STAC_layerwise.generate_raster_stac( + state=state, + district=district, + block=block, + layer_name="catchment_area_raster", + ) + update_layer_sync_status( + layer_id=raster_layer_id, + is_stac_specs_generated=layer_STAC_generated, + ) + print("STAC metadata updated for Catchment Area raster") + except Exception as e: + print(f"Error generating STAC for raster: {e}") + + return layer_at_geoserver diff --git a/computing/misc/digital_elevation_model_local.py b/computing/misc/digital_elevation_model_local.py new file mode 100644 index 00000000..53e737b5 --- /dev/null +++ b/computing/misc/digital_elevation_model_local.py @@ -0,0 +1,324 @@ +import os +import rasterio +from rasterio.mask import mask +from shapely.geometry import mapping + +from utilities.gee_utils import valid_gee_text +from nrm_app.celery import app +from computing.utils import push_shape_to_geoserver + +from computing.local_compute_helper import ( + PROJECT_ROOT, + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + build_output_raster_path, + build_output_vector_path, + get_union_geometry, + load_precomputed_roi, + load_precomputed_watersheds, + push_local_raster_to_geoserver, + read_validated_vector_file, + write_vector_output, +) + + +from computing.config_loader import ( + PAN_INDIA_FABDEM_PATH, + LOCAL_FABDEM_OUTPUT, +) + +GEOSERVER_STYLE = None +GEOSERVER_WORKSPACE = "dem" +ZERO_NODATA = -9999 + + +def _clip_fabdem_with_roi(roi_gdf, output_path): + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + with rasterio.open(PAN_INDIA_FABDEM_PATH) as src: + raster_crs = src.crs + + # Reproject ROI to raster CRS — use pyproj data dir to avoid broken PROJ + if roi_gdf.crs != raster_crs: + roi_in_raster_crs = roi_gdf.to_crs("EPSG:3857") + else: + roi_in_raster_crs = roi_gdf + + roi_union = get_union_geometry(roi_in_raster_crs) + if roi_union is None or roi_union.is_empty: + raise ValueError("ROI union geometry is empty — cannot clip FABDEM.") + + roi_shape = mapping(roi_union) + + clipped_array, clipped_transform = mask( + src, + shapes=[roi_shape], + crop=True, + filled=True, + nodata=ZERO_NODATA, + ) + out_meta = src.meta.copy() + out_meta.update( + { + "driver": "GTiff", + "height": clipped_array.shape[1], + "width": clipped_array.shape[2], + "transform": clipped_transform, + "nodata": ZERO_NODATA, + "compress": "lzw", + } + ) + + with rasterio.open(output_path, "w", **out_meta) as dst: + dst.write(clipped_array) + + print(f"Local clipped FABDEM raster written to: {output_path}") + return str(output_path) + + +def run_raster_fabdem_local( + state=None, + district=None, + block=None, + asset_suffix=None, + roi=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=False, +): + if state and district and block: + layer_name = f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_dem_raster" + roi_gdf = load_precomputed_roi( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + else: + if not roi or not asset_suffix: + raise ValueError( + "For non state/district/block runs, both `roi` and `asset_suffix` are required." + ) + layer_name = f"{asset_suffix}_dem_raster".lower() + roi_gdf = read_validated_vector_file( + roi, + f"ROI file has no valid geometries: {roi}", + ) + + output_raster_path = build_output_raster_path( + layer_name=layer_name, + output_base_dir=LOCAL_FABDEM_OUTPUT, + state=state, + district=district, + block=block, + ) + + clipped_raster_path = _clip_fabdem_with_roi( + roi_gdf=roi_gdf, + output_path=str(output_raster_path), + ) + + if push_to_geoserver: + try: + upload_res, style_res = push_local_raster_to_geoserver( + file_path=clipped_raster_path, + layer_name=layer_name, + workspace=GEOSERVER_WORKSPACE, + style_name=GEOSERVER_STYLE, + ) + print(f"GeoServer upload response: {upload_res}") + print(f"GeoServer style response: {style_res}") + except Exception as error: + print(f"Failed to sync local FABDEM raster to GeoServer: {error}") + return False, None + + if sync_layer_metadata and state and district and block: + from computing.STAC_specs import generate_STAC_layerwise + from computing.utils import save_layer_info_to_db, update_layer_sync_status + + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=clipped_raster_path, + dataset_name="DEM Raster", + algorithm="FABDEM", + algorithm_version="1.0", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated") + + return True, clipped_raster_path + + +def _compute_watershed_dem_stats(watersheds_gdf, raster_path): + from rasterstats import zonal_stats + + with rasterio.open(raster_path) as src: + raster_crs = src.crs + pixel_area_ha = (abs(src.res[0]) * abs(src.res[1])) / 10_000.0 + + # Reproject watersheds to raster CRS for accurate zonal stats + watersheds_for_stats = ( + watersheds_gdf + if watersheds_gdf.crs == raster_crs + else watersheds_gdf.to_crs(raster_crs.to_epsg()) + ) + + stats = zonal_stats( + watersheds_for_stats, + raster_path, + stats=["min", "max", "mean", "count"], + nodata=ZERO_NODATA, + all_touched=False, + ) + + result_gdf = watersheds_gdf.copy() + result_gdf["min_elevation"] = [s.get("min") for s in stats] + result_gdf["max_elevation"] = [s.get("max") for s in stats] + result_gdf["mean_elevation"] = [s.get("mean") for s in stats] + + keep_cols = [ + "uid", + "min_elevation", + "max_elevation", + "mean_elevation", + "geometry", + ] + return result_gdf[[c for c in keep_cols if c in result_gdf.columns]] + + +def run_vector_fabdem_local( + state=None, + district=None, + block=None, + asset_suffix=None, + raster_path=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=False, +): + if not raster_path: + raise ValueError( + "`raster_path` is required for vector stage — pass Stage 1 output." + ) + + if state and district and block: + layer_name = f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_dem_vector" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + if not asset_suffix: + raise ValueError( + "For non state/district/block runs, `asset_suffix` is required." + ) + layer_name = f"{asset_suffix}_dem_vector".lower() + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + + result_gdf = _compute_watershed_dem_stats(watersheds_gdf, raster_path) + print(f"Computed DEM stats for {len(result_gdf)} watersheds") + + output_path = build_output_vector_path( + layer_name=layer_name, + output_base_dir=LOCAL_FABDEM_OUTPUT, + state=state, + district=district, + block=block, + ) + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local DEM vector: {asset_id}") + + if push_to_geoserver: + try: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer vector response: {geoserver_response}") + if not isinstance(geoserver_response, dict) or geoserver_response.get( + "status_code" + ) not in (200, 201): + return False + except Exception as error: + print(f"Failed to sync local FABDEM vector to GeoServer: {error}") + return False + + if sync_layer_metadata and state and district and block: + from computing.utils import save_layer_info_to_db, update_layer_sync_status + + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="DEM Vector", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for DEM vector") + + return True + + +@app.task(bind=True) +def generate_febdem_raster_vector_clip( + self, + state=None, + district=None, + block=None, + gee_account_id=None, + asset_suffix=None, + asset_folder=None, + proj_id=None, + roi=None, + precomputed_roi_dir=None, + app_type="MWS", +): + raster_ok, clipped_raster_path = run_raster_fabdem_local( + state=state, + district=district, + block=block, + asset_suffix=asset_suffix, + roi=roi, + precomputed_roi_dir=precomputed_roi_dir, + push_to_geoserver=True, + sync_layer_metadata=True, + ) + + if not raster_ok or not clipped_raster_path: + print("Raster stage failed — skipping vector stage.") + return False + + vector_ok = run_vector_fabdem_local( + state=state, + district=district, + block=block, + asset_suffix=asset_suffix, + raster_path=clipped_raster_path, + precomputed_roi_dir=precomputed_roi_dir, + push_to_geoserver=True, + sync_layer_metadata=True, + ) + + return raster_ok and vector_ok diff --git a/computing/misc/distancetonearestdrainage_local_compute.py b/computing/misc/distancetonearestdrainage_local_compute.py new file mode 100644 index 00000000..1b8c016f --- /dev/null +++ b/computing/misc/distancetonearestdrainage_local_compute.py @@ -0,0 +1,119 @@ +import os +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.utils import ( + save_layer_info_to_db, + update_layer_sync_status, +) +from computing.local_compute_helper import ( + PROJECT_ROOT, + build_output_raster_path, + load_precomputed_watersheds, + read_validated_vector_file, + clip_raster_with_roi, + push_local_raster_to_geoserver, +) +from computing.STAC_specs import generate_STAC_layerwise +from computing.config_loader import ( + PAN_INDIA_DISTANCETONEARESTDRAINAGE_PATH, + LOCAL_DISTANCETONEARESTDRAINAGE_OUTPUT, +) + +GEOSERVER_WORKSPACE = "distance_nearest_upstream_DL" +DISTANCE_TO_DRAINAGE_STYLE_NAME = "distance_nearest_upstream_DL" + + +@app.task(bind=True) +def generate_distance_to_nearest_drainage_line_local( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + gee_account_id=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if state and district and block: + layer_name_base = f"distance_to_drainage_line_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_raster" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + if not roi_path or not asset_suffix: + raise ValueError("ROI path and asset_suffix are required for custom runs.") + layer_name_base = ( + f"distance_to_drainage_line_{valid_gee_text(asset_suffix).lower()}_raster" + ) + watersheds_gdf = read_validated_vector_file( + roi_path, f"Invalid ROI file: {roi_path}" + ) + print(f"ROI source: {roi_path}") + + # Raster Processing + raster_layer_name = f"{layer_name_base}" + output_raster_path = build_output_raster_path( + layer_name=raster_layer_name, + output_base_dir=LOCAL_DISTANCETONEARESTDRAINAGE_OUTPUT, + state=state, + district=district, + block=block, + ) + + print("Clipping Distance to Nearest Drainage Line raster...") + clipped_raster_path = clip_raster_with_roi( + roi_gdf=watersheds_gdf, + raster_path=PAN_INDIA_DISTANCETONEARESTDRAINAGE_PATH, + output_path=output_raster_path, + raster_label="Distance to Drainage Line Raster", + ) + print(f"Saved clipped raster to: {clipped_raster_path}") + + layer_at_geoserver = False + if push_to_geoserver: + push_local_raster_to_geoserver( + file_path=str(clipped_raster_path), + layer_name=raster_layer_name, + workspace=GEOSERVER_WORKSPACE, + style_name=DISTANCE_TO_DRAINAGE_STYLE_NAME, + ) + print(f"Pushed raster {raster_layer_name} to GeoServer.") + layer_at_geoserver = True + + if sync_layer_metadata and state and district and block: + raster_layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=raster_layer_name, + asset_id=str(clipped_raster_path), + dataset_name="Distance to Drainage Line", + misc={"is_generated_locally": True}, + ) + if raster_layer_id: + update_layer_sync_status(layer_id=raster_layer_id, sync_to_geoserver=True) + print(f"Database record updated for raster layer_id: {raster_layer_id}") + + # STAC Specs for Raster + try: + layer_STAC_generated = generate_STAC_layerwise.generate_raster_stac( + state=state, + district=district, + block=block, + layer_name="distance_to_nearest_drainage_line_raster", + ) + update_layer_sync_status( + layer_id=raster_layer_id, + is_stac_specs_generated=layer_STAC_generated, + ) + print("STAC metadata updated for Distance to Drainage Line raster") + except Exception as e: + print(f"Error generating STAC for raster: {e}") + + return layer_at_geoserver diff --git a/computing/misc/drainage_density_local_compute.py b/computing/misc/drainage_density_local_compute.py new file mode 100644 index 00000000..3591194b --- /dev/null +++ b/computing/misc/drainage_density_local_compute.py @@ -0,0 +1,188 @@ +import os +import geopandas as gpd +from shapely.geometry import box + +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) +from computing.local_compute_helper import ( + build_output_vector_path, + load_precomputed_watersheds, + read_validated_vector_file, + write_vector_output, +) + +from computing.config_loader import ( + PAN_INDIA_DRAINAGE_LINES_GPKG_PATH, + LOCAL_DRAINAGE_DENSITY_OUTPUT, +) + +GEOSERVER_WORKSPACE = "drainage_density" + +# Influence factors for stream orders 1 to 11 +INFLUENCE_FACTORS = [ + 60 / 385, + 55 / 385, + 50 / 385, + 45 / 385, + 40 / 385, + 35 / 385, + 30 / 385, + 25 / 385, + 20 / 385, + 15 / 385, + 10 / 385, +] + + +def _load_drainage_lines_for_roi(watersheds_gdf): + bounds = watersheds_gdf.geometry.total_bounds + bbox_geom = box(*bounds) + + print(f"Loading drainage lines from: {PAN_INDIA_DRAINAGE_LINES_GPKG_PATH}") + if not os.path.exists(PAN_INDIA_DRAINAGE_LINES_GPKG_PATH): + # Fallback to checking common locations if the exact path is missing + print( + f"Warning: {PAN_INDIA_DRAINAGE_LINES_GPKG_PATH} not found. Drainage density calculation will fail." + ) + + lines_gdf = gpd.read_file(PAN_INDIA_DRAINAGE_LINES_GPKG_PATH, bbox=bbox_geom) + print(f"Loaded {len(lines_gdf)} drainage line features within bounding box") + return lines_gdf + + +def _compute_drainage_density(watersheds_gdf, drainage_lines_gdf): + """ + Core calculation logic for Drainage Density. + Matches the GEE version's methodology. + """ + # Reproject to metric CRS for accurate length/area calculation + # (7755 is India-specific metric projection) + drainage_lines_gdf = drainage_lines_gdf.to_crs(crs=7755) + watersheds_gdf = watersheds_gdf.to_crs(crs=7755) + + for index, watershed in watersheds_gdf.iterrows(): + # Clip drainage lines to this watershed boundary + clipped_lines = gpd.clip(drainage_lines_gdf, watershed.geometry) + + # Area in km² (area_in_ha / 100) + area_km2 = watershed["area_in_ha"] / 100 + if area_km2 <= 0: + continue + + stream_length = {} + stream_dd = {} + + for stream_order, factor in zip(range(1, 12), INFLUENCE_FACTORS): + # Filter lines for this stream order + order_lines = clipped_lines[clipped_lines["ORDER"] == stream_order] + # Total length in km + length_km = order_lines.geometry.length.sum() / 1000 + # Weighted drainage density for this stream order (formula from GEE version) + dd = length_km * factor * 100 / area_km2 + + stream_length[stream_order] = length_km + stream_dd[stream_order] = dd + + # Store results as strings of lists to match GEE output + watersheds_gdf.at[index, "drainage_density_std"] = (float(sum(stream_length.values())) / area_km2) + watersheds_gdf.at[index, "drainage_density_weighted"] = float(sum(stream_dd.values())) + watersheds_gdf.at[index, "drainage_density_stream"] = str( + [float(v) for v in stream_dd.values()] + ) + watersheds_gdf.at[index, "stream_length_km"] = str( + [float(v) for v in stream_length.values()] + ) + + # Restore geographic CRS + watersheds_gdf = watersheds_gdf.to_crs(crs=4326) + return watersheds_gdf + + +@app.task(bind=True) +def drainage_density( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi=None, + app_type="MWS", + gee_account_id=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + """ + Main entry point for local drainage density computation. + Produces MWS polygons with drainage_density attributes. + """ + if state and district and block: + layer_name = f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_drainage_density" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Loaded watersheds from {watershed_source}") + else: + if not roi or not asset_suffix: + raise ValueError("ROI and asset_suffix are required for custom runs.") + layer_name = f"{asset_suffix}_drainage_density_vector".lower() + watersheds_gdf = read_validated_vector_file(roi, f"Invalid ROI file: {roi}") + + # 1. Load drainage lines + try: + drainage_lines_gdf = _load_drainage_lines_for_roi(watersheds_gdf) + except Exception as e: + print(f"Error loading drainage lines: {e}") + return False + + print("Computing drainage density per watershed...") + result_gdf = _compute_drainage_density(watersheds_gdf, drainage_lines_gdf) + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_DRAINAGE_DENSITY_OUTPUT, + ) + + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local drainage_density vector: {asset_id}") + + # 4. Push to GeoServer + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + + # 5. Sync to database + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Drainage Density Vector", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print(f"Sync Data for layer_id: {layer_id}") + + return True diff --git a/computing/misc/drainage_lines_local_compute.py b/computing/misc/drainage_lines_local_compute.py new file mode 100644 index 00000000..2f147af5 --- /dev/null +++ b/computing/misc/drainage_lines_local_compute.py @@ -0,0 +1,149 @@ +import os +import geopandas as gpd +from shapely.geometry import box +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.local_compute_helper import ( + PROJECT_ROOT, + build_output_vector_path, + load_precomputed_watersheds, + read_validated_vector_file, + validate_geometry, + write_vector_output, +) +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, + fix_invalid_geometry_in_gdf, +) +from projects.models import Project + +from computing.config_loader import ( + PAN_INDIA_DRAINAGE_LINES_PATH, + LOCAL_DRAINAGE_LINES_OUTPUT, +) + +GEOSERVER_WORKSPACE = "drainage" + + +def _compute_drainage_lines_for_watersheds(watersheds_gdf, drainage_gdf): + watersheds_gdf = validate_geometry(watersheds_gdf).reset_index(drop=True) + drainage_gdf = validate_geometry(drainage_gdf).reset_index(drop=True) + + outer_boundary = watersheds_gdf.geometry.unary_union + + # Step 1: Filter drainage features that intersect the ROI (equivalent to GEE filterBounds) + drainage_in_roi = drainage_gdf[drainage_gdf.intersects(outer_boundary)].copy() + + if drainage_in_roi.empty: + print("No drainage lines found within the outer boundary.") + return gpd.GeoDataFrame(columns=drainage_gdf.columns, crs=drainage_gdf.crs) + + print(f"Drainage lines within outer boundary: {len(drainage_in_roi)}") + + # Step 2: Drop empty/invalid geometries + drainage_in_roi = fix_invalid_geometry_in_gdf(drainage_in_roi) + drainage_in_roi = drainage_in_roi[ + drainage_in_roi.geometry.notna() + & ~drainage_in_roi.geometry.is_empty + & drainage_in_roi.geometry.is_valid + ] + + print(f"Final valid drainage lines: {len(drainage_in_roi)}") + return drainage_in_roi + + +@app.task(bind=True) +def clip_drainage_lines( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + asset_folder=None, + gee_account_id=None, + roi_path=None, + app_type="MWS", + proj_id=None, + drainage_lines_path=PAN_INDIA_DRAINAGE_LINES_PATH, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + """ + Celery task for local drainage lines vector generation. + """ + + if state and district and block: + layer_name = f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + proj_obj = Project.objects.get(pk=proj_id) + state = proj_obj.name + layer_name = asset_suffix + watersheds_gdf = read_validated_vector_file( + roi_path, + f"ROI file has no valid geometries: {roi_path}", + ) + print(f"ROI source: {roi_path}") + + if not os.path.exists(drainage_lines_path): + raise FileNotFoundError(f"PAN INDIA drainage lines file not found at {drainage_lines_path}") + + bounds = watersheds_gdf.geometry.total_bounds + bbox_geom = box(*bounds) + drainage_gdf = gpd.read_file(drainage_lines_path, bbox=bbox_geom) + + result_gdf = _compute_drainage_lines_for_watersheds( + watersheds_gdf=watersheds_gdf, + drainage_gdf=drainage_gdf, + ) + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_DRAINAGE_LINES_OUTPUT, + ) + + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local drainage lines vector: {asset_id}") + + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer response: {geoserver_response}") + + if sync_layer_metadata: + layer_id = None + if state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Drainage", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for drainage lines vector") + + return True diff --git a/computing/misc/facilities/pipeline.py b/computing/misc/facilities/pipeline.py index 413b3fec..40acc71c 100644 --- a/computing/misc/facilities/pipeline.py +++ b/computing/misc/facilities/pipeline.py @@ -16,6 +16,7 @@ from django.conf import settings from scipy.spatial import cKDTree +from computing.utils import save_layer_info_to_db, update_layer_sync_status from utilities.pipelines import AdminScope, CSAdminSource, StandardRequest, load_config from utilities.pipelines.admin import ( ADMIN_COLUMN_DESCRIPTIONS, @@ -45,7 +46,7 @@ frame_profile, input_signatures, mark_cached_result, - scope_output_identity, + resolved_scope_output_identity, slug, stable_hash, utc_now_text, @@ -53,7 +54,6 @@ from utilities.pipelines.publish import ( publish_gpkg_layer, publish_gpkg_layers, - register_layer, ) from utilities.pipelines.unicode import normalize_unicode_frame from nrm_app.celery import app @@ -62,11 +62,11 @@ FACILITIES_GEOSERVER_WORKSPACE, FACILITIES_GPKG, ) - +from utilities.pipelines import api_request_payload CONFIG_PATH = Path(__file__).with_name("facilities_pipeline.yaml") ALGORITHM = "local-facilities-live-proximity" -ALGORITHM_VERSION = "2.0" +ALGORITHM_VERSION = "2.1" SOURCE_DEFAULTS = { "admin_gpkg": ADMIN_BOUNDARY_GPKG, "facilities_gpkg": FACILITIES_GPKG, @@ -92,11 +92,9 @@ def _apply_source_defaults(config: Mapping[str, Any]) -> dict[str, Any]: return resolved -def _layer_name(prefix: str, district: str | None, tehsil: str | None) -> str: - return f"{prefix}_{slug(district)}_{slug(tehsil)}".strip("_") - - -def _cli_request(state: str, district: str, tehsil: str, sync_to_geoserver: bool = True) -> StandardRequest: +def _cli_request( + state: str, district: str, tehsil: str, sync_to_geoserver: bool = True +) -> StandardRequest: return StandardRequest.from_mapping( { "scope": { @@ -143,8 +141,12 @@ def _taxonomy(config: Mapping[str, Any]) -> pd.DataFrame: "class_l2_filter_group": item.get("class_l2_filter_group"), "class_l3_facility_class": item.get("key"), "class_l3_label": item.get("label"), - "configured_subtypes": ";".join(item.get("configured_subtypes", []) or []), - "filter_logic": l2_rollups.get(item.get("class_l2_filter_group"), "direct"), + "configured_subtypes": ";".join( + item.get("configured_subtypes", []) or [] + ), + "filter_logic": l2_rollups.get( + item.get("class_l2_filter_group"), "direct" + ), "sort_order": sort_order, } ) @@ -168,13 +170,25 @@ def _classification(config: Mapping[str, Any]) -> dict[str, Any]: taxonomy = _taxonomy(config) l2_groups: list[dict[str, Any]] = [] for group, rows in taxonomy.groupby("class_l2_filter_group", sort=False): - logic_values = [value for value in rows.get("filter_logic", pd.Series(dtype=str)).dropna().astype(str).unique() if value] + logic_values = [ + value + for value in rows.get("filter_logic", pd.Series(dtype=str)) + .dropna() + .astype(str) + .unique() + if value + ] l2_groups.append( { "key": group, "label": str(group).replace("_", " ").title(), "rollup": logic_values[0] if logic_values else "direct", - "class_l1_domain": rows["class_l1_domain"].dropna().iloc[0] if "class_l1_domain" in rows and not rows["class_l1_domain"].dropna().empty else None, + "class_l1_domain": ( + rows["class_l1_domain"].dropna().iloc[0] + if "class_l1_domain" in rows + and not rows["class_l1_domain"].dropna().empty + else None + ), } ) return { @@ -242,7 +256,9 @@ def _read_facilities_bbox( frame = pd.read_sql_query(sql, connection, params=params) if frame.empty: return gpd.GeoDataFrame(frame, geometry=[], crs="EPSG:4326") - geometry = gpd.points_from_xy(frame["longitude"], frame["latitude"], crs="EPSG:4326") + geometry = gpd.points_from_xy( + frame["longitude"], frame["latitude"], crs="EPSG:4326" + ) return gpd.GeoDataFrame(frame, geometry=geometry) @@ -290,7 +306,10 @@ def _haversine_km(lat1, lon1, lat2, lon2, radius_km: float) -> float: lon2 = math.radians(float(lon2)) dlat = lat2 - lat1 dlon = lon2 - lon1 - a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2 + a = ( + math.sin(dlat / 2) ** 2 + + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2 + ) return 2 * radius_km * math.asin(math.sqrt(a)) @@ -300,9 +319,15 @@ def _candidate_pool( taxonomy: pd.DataFrame, ) -> tuple[gpd.GeoDataFrame, dict[str, Any]]: search = config["search"] - base = _read_facilities_bbox(config, bbox, expansion_degrees=float(search["base_expansion_degrees"])) + base = _read_facilities_bbox( + config, bbox, expansion_degrees=float(search["base_expansion_degrees"]) + ) required = taxonomy["class_l3_facility_class"].dropna().astype(str).tolist() - covered = set(base["class_l3_facility_class"].dropna().astype(str)) if not base.empty else set() + covered = ( + set(base["class_l3_facility_class"].dropna().astype(str)) + if not base.empty + else set() + ) missing = [value for value in required if value not in covered] supplemental = gpd.GeoDataFrame(pd.DataFrame(), geometry=[], crs="EPSG:4326") if missing: @@ -312,17 +337,30 @@ def _candidate_pool( class_l3_values=missing, expansion_degrees=float(search["supplemental_expansion_degrees"]), ) - frames = [frame.dropna(axis=1, how="all") for frame in (base, supplemental) if not frame.empty] + frames = [ + frame.dropna(axis=1, how="all") + for frame in (base, supplemental) + if not frame.empty + ] pool = pd.concat(frames, ignore_index=True) if frames else pd.DataFrame() if pool.empty: pool = gpd.GeoDataFrame(pool, geometry=[], crs="EPSG:4326") else: - pool = gpd.GeoDataFrame(pool, geometry="geometry", crs="EPSG:4326").drop_duplicates(subset=["facility_uid"]) + pool = gpd.GeoDataFrame( + pool, geometry="geometry", crs="EPSG:4326" + ).drop_duplicates(subset=["facility_uid"]) metadata = { "base_candidates": int(len(base)), "supplemental_candidates": int(len(supplemental)), "candidate_pool": int(len(pool)), - "missing_after_supplemental": sorted(set(required) - set(pool["class_l3_facility_class"].dropna().astype(str))) if not pool.empty else required, + "missing_after_supplemental": ( + sorted( + set(required) + - set(pool["class_l3_facility_class"].dropna().astype(str)) + ) + if not pool.empty + else required + ), } return pool, metadata @@ -336,12 +374,17 @@ def _nearest( radius_km: float, ) -> tuple[gpd.GeoDataFrame, pd.DataFrame]: if pool.empty or village_points.empty: - return gpd.GeoDataFrame(pd.DataFrame(), geometry=[], crs="EPSG:4326"), pd.DataFrame() + return ( + gpd.GeoDataFrame(pd.DataFrame(), geometry=[], crs="EPSG:4326"), + pd.DataFrame(), + ) nearest_rows: list[dict[str, Any]] = [] point_xy = village_points[["_village_lon", "_village_lat"]].to_numpy() for tax in taxonomy.itertuples(index=False): class_l3 = str(tax.class_l3_facility_class) - candidates = pool[pool["class_l3_facility_class"] == class_l3].reset_index(drop=True) + candidates = pool[pool["class_l3_facility_class"] == class_l3].reset_index( + drop=True + ) if candidates.empty: continue tree = cKDTree(candidates[["longitude", "latitude"]].to_numpy()) @@ -371,9 +414,13 @@ def _nearest( "class_l1_domain": getattr(tax, "class_l1_domain"), "class_l2_filter_group": getattr(tax, "class_l2_filter_group"), "class_l3_facility_class": class_l3, - "class_l3_label": getattr(tax, "class_l3_label", class_l3.replace("_", " ").title()), + "class_l3_label": getattr( + tax, "class_l3_label", class_l3.replace("_", " ").title() + ), "nearest_distance_km": round(distance_km, 6), - "inside_requested_scope": bool(facility["facility_uid"] in inside_facility_ids), + "inside_requested_scope": bool( + facility["facility_uid"] in inside_facility_ids + ), "facilities_layer_kind": "nearest", "title": facility.get("facility_name") or facility.get("facility_uid"), } @@ -395,7 +442,11 @@ def _nearest( return nearest, village_service -def _village_service(village_points: pd.DataFrame, nearest: pd.DataFrame, classification: Mapping[str, Any]) -> pd.DataFrame: +def _village_service( + village_points: pd.DataFrame, + nearest: pd.DataFrame, + classification: Mapping[str, Any], +) -> pd.DataFrame: base = village_points.drop(columns=["_village_lon", "_village_lat"]).copy() if nearest.empty: return base @@ -406,7 +457,9 @@ def _village_service(village_points: pd.DataFrame, nearest: pd.DataFrame, classi ("facility_uid", "facility_uid"), ("inside_requested_scope", "inside_scope"), ): - pivot = l3.pivot_table(index="_admin_key", columns="_slug", values=metric, aggfunc="first") + pivot = l3.pivot_table( + index="_admin_key", columns="_slug", values=metric, aggfunc="first" + ) pivot.columns = [f"l3_{col}_{suffix}" for col in pivot.columns] base = base.merge(pivot.reset_index(), on="_admin_key", how="left") l2_rollups = { @@ -433,7 +486,9 @@ def _village_service(village_points: pd.DataFrame, nearest: pd.DataFrame, classi ("class_l3_facility_class", "selected_l3"), ("class_l3_label", "selected_l3_label"), ): - pivot = l2.pivot_table(index="_admin_key", columns="_slug", values=metric, aggfunc="first") + pivot = l2.pivot_table( + index="_admin_key", columns="_slug", values=metric, aggfunc="first" + ) pivot.columns = [f"l2_{col}_{suffix}" for col in pivot.columns] base = base.merge(pivot.reset_index(), on="_admin_key", how="left") base["facilities_layer_kind"] = "village_service" @@ -467,8 +522,17 @@ def _facility_detail(row: pd.Series | None) -> str | None: if row is None: return None parts: list[str] = [] - for column in ("facility_name", "class_l4_facility_subtype", "class_l3_label", "facility_code"): - value = _clean_facility_text(row.get(column)) if column != "facility_code" else _useful_value(row.get(column)) + for column in ( + "facility_name", + "class_l4_facility_subtype", + "class_l3_label", + "facility_code", + ): + value = ( + _clean_facility_text(row.get(column)) + if column != "facility_code" + else _useful_value(row.get(column)) + ) if value: parts.append(value if column != "facility_code" else f"code {value}") scope_value = row.get("inside_requested_scope") @@ -489,14 +553,18 @@ def _l2_output_column(group: Mapping[str, Any], config: Mapping[str, Any]) -> st return f"{slug(key)}_cat_distance_km" -def _l3_classes_by_group(classification: Mapping[str, Any]) -> dict[str, list[Mapping[str, Any]]]: +def _l3_classes_by_group( + classification: Mapping[str, Any], +) -> dict[str, list[Mapping[str, Any]]]: grouped: dict[str, list[Mapping[str, Any]]] = {} for item in classification.get("l3_classes", []): grouped.setdefault(item["class_l2_filter_group"], []).append(item) return grouped -def _service_output_columns(classification: Mapping[str, Any], config: Mapping[str, Any]) -> list[str]: +def _service_output_columns( + classification: Mapping[str, Any], config: Mapping[str, Any] +) -> list[str]: """Report columns derived from the classification structure: for each L2 group the category distance column, then the nearest-facility detail and distance pair for each of its L3 classes.""" @@ -545,7 +613,9 @@ def _machine_output_columns(classification: Mapping[str, Any]) -> list[str]: return columns -def _column_descriptions(classification: Mapping[str, Any], config: Mapping[str, Any]) -> dict[str, str]: +def _column_descriptions( + classification: Mapping[str, Any], config: Mapping[str, Any] +) -> dict[str, str]: """Human-readable descriptions for report columns, driven by the classification YAML description templates.""" @@ -561,31 +631,59 @@ def _column_descriptions(classification: Mapping[str, Any], config: Mapping[str, label = str(group.get("label") or group["key"]) template = category_templates.get(rollup) if template: - descriptions[_l2_output_column(group, config)] = str(template).format(label=label) + descriptions[_l2_output_column(group, config)] = str(template).format( + label=label + ) for item in l3_by_l2.get(group["key"], []): l3_label = str(item.get("label") or item["key"]) l3_slug = slug(item["key"]) if templates.get("nearest_facility"): - descriptions[f"nearest_{l3_slug}"] = str(templates["nearest_facility"]).format(label=l3_label) + descriptions[f"nearest_{l3_slug}"] = str( + templates["nearest_facility"] + ).format(label=l3_label) if templates.get("nearest_distance"): - descriptions[f"nearest_{l3_slug}_distance_km"] = str(templates["nearest_distance"]).format(label=l3_label) + descriptions[f"nearest_{l3_slug}_distance_km"] = str( + templates["nearest_distance"] + ).format(label=l3_label) return descriptions -def _machine_column_describer(classification: Mapping[str, Any], config: Mapping[str, Any]): +def _machine_column_describer( + classification: Mapping[str, Any], config: Mapping[str, Any] +): """Describe both report columns and the verbose `l2_*`/`l3_*` GPKG columns.""" descriptions = _column_descriptions(classification, config) - labels = {slug(item["key"]): str(item.get("label") or item["key"]) for item in classification.get("l3_classes", [])} - labels.update({slug(group["key"]): str(group.get("label") or group["key"]) for group in classification.get("l2_groups", [])}) + labels = { + slug(item["key"]): str(item.get("label") or item["key"]) + for item in classification.get("l3_classes", []) + } + labels.update( + { + slug(group["key"]): str(group.get("label") or group["key"]) + for group in classification.get("l2_groups", []) + } + ) patterns = ( - ("l2_", "_selected_l3_label", "Label of the L3 facility class selected for the {label} group."), + ( + "l2_", + "_selected_l3_label", + "Label of the L3 facility class selected for the {label} group.", + ), ("l2_", "_selected_l3", "L3 facility class selected for the {label} group."), ("l2_", "_distance_km", "Access distance in km for the {label} group."), - ("l2_", "_facility_uid", "Identifier of the facility selected for the {label} group."), + ( + "l2_", + "_facility_uid", + "Identifier of the facility selected for the {label} group.", + ), ("l3_", "_distance_km", "Distance in km to the nearest {label}."), ("l3_", "_facility_uid", "Identifier of the nearest {label}."), - ("l3_", "_inside_scope", "Whether the nearest {label} lies inside the requested boundary."), + ( + "l3_", + "_inside_scope", + "Whether the nearest {label} lies inside the requested boundary.", + ), ) def describe(name: str) -> str | None: @@ -605,16 +703,24 @@ def describe(name: str) -> str | None: return describe -def _machine_column_renamer(classification: Mapping[str, Any], config: Mapping[str, Any]): +def _machine_column_renamer( + classification: Mapping[str, Any], config: Mapping[str, Any] +): """Return optional report-facing names without changing stored fields.""" l2_targets: dict[str, str] = {} for group in classification.get("l2_groups", []): group_slug = slug(group["key"]) l2_targets[f"l2_{group_slug}_distance_km"] = _l2_output_column(group, config) - l2_targets[f"l2_{group_slug}_facility_uid"] = f"{group_slug}_selected_facility_uid" - l2_targets[f"l2_{group_slug}_selected_l3"] = f"{group_slug}_selected_facility_class" - l2_targets[f"l2_{group_slug}_selected_l3_label"] = f"{group_slug}_selected_facility_label" + l2_targets[f"l2_{group_slug}_facility_uid"] = ( + f"{group_slug}_selected_facility_uid" + ) + l2_targets[f"l2_{group_slug}_selected_l3"] = ( + f"{group_slug}_selected_facility_class" + ) + l2_targets[f"l2_{group_slug}_selected_l3_label"] = ( + f"{group_slug}_selected_facility_label" + ) def rename(name: str) -> str | None: if name in l2_targets: @@ -647,7 +753,9 @@ def _canonical_facility_point_output(frame: gpd.GeoDataFrame) -> gpd.GeoDataFram "NAME": "village_name", } ) - admin_columns = [column for column in ADMIN_PRESENTATION_COLUMNS if column in output.columns] + admin_columns = [ + column for column in ADMIN_PRESENTATION_COLUMNS if column in output.columns + ] value_columns = [ column for column in output.columns @@ -743,14 +851,20 @@ def _focused_service_frame( nearest_by_village_l3: dict[tuple[Any, str], pd.Series] = {} if not nearest.empty: for _, row in nearest.iterrows(): - nearest_by_village_l3[(row.get("_admin_key"), row.get("class_l3_facility_class"))] = row + nearest_by_village_l3[ + (row.get("_admin_key"), row.get("class_l3_facility_class")) + ] = row rows: list[dict[str, Any]] = [] l3_by_l2 = _l3_classes_by_group(classification) for _, source in frame.iterrows(): row = source.to_dict() admin_key = row.pop("_admin_key", None) - service = service_rows.loc[admin_key] if admin_key in service_rows.index else pd.Series(dtype=object) + service = ( + service_rows.loc[admin_key] + if admin_key in service_rows.index + else pd.Series(dtype=object) + ) has_village_id = pd.notna(row.get("village_id")) if status_name: row[status_name] = _status_value(row.get("village_id"), not nearest.empty) @@ -761,13 +875,19 @@ def _focused_service_frame( for group in classification.get("l2_groups", []): group_key = group["key"] group_slug = slug(group_key) - row[_l2_output_column(group, config)] = service.get(f"l2_{group_slug}_distance_km") + row[_l2_output_column(group, config)] = service.get( + f"l2_{group_slug}_distance_km" + ) for item in l3_by_l2.get(group_key, []): l3_key = item["key"] l3_slug = slug(l3_key) nearest_row = nearest_by_village_l3.get((admin_key, l3_key)) row[f"nearest_{l3_slug}"] = _facility_detail(nearest_row) - row[f"nearest_{l3_slug}_distance_km"] = None if nearest_row is None else nearest_row.get("nearest_distance_km") + row[f"nearest_{l3_slug}_distance_km"] = ( + None + if nearest_row is None + else nearest_row.get("nearest_distance_km") + ) rows.append(row) ordered = [*admin_columns] if status_name: @@ -784,9 +904,13 @@ def _column_reference_lines(column_entries: list[Mapping[str, Any]]) -> list[str "| --- | --- | --- | --- |", ] for entry in column_entries: - description = str(entry.get("description") or "").replace("|", "\\|").replace("\n", " ") + description = ( + str(entry.get("description") or "").replace("|", "\\|").replace("\n", " ") + ) rename_to = entry.get("rename_to") or "" - lines.append(f"| `{entry['column']}` | {entry.get('datatype', '')} | {rename_to} | {description} |") + lines.append( + f"| `{entry['column']}` | {entry.get('datatype', '')} | {rename_to} | {description} |" + ) lines.append("") return lines @@ -854,7 +978,9 @@ def _readme_lines( return lines -def _cache_input_signatures(config: Mapping[str, Any], config_path: str | Path) -> dict[str, dict[str, Any]]: +def _cache_input_signatures( + config: Mapping[str, Any], config_path: str | Path +) -> dict[str, dict[str, Any]]: sources = config.get("sources", {}) paths: dict[str, str | Path] = { "pipeline_config": _repo_path(config_path), @@ -880,7 +1006,9 @@ def _cache_key(request: StandardRequest, outputs: OutputOptions) -> str: ) -def _required_result_paths(outputs: OutputOptions, request: StandardRequest) -> tuple[str, ...]: +def _required_result_paths( + outputs: OutputOptions, request: StandardRequest +) -> tuple[str, ...]: required: list[str] = ["links_path"] if outputs.metadata: required.append("run_metadata_path") @@ -901,7 +1029,25 @@ def run_facilities_pipeline( config = _apply_source_defaults(load_config(config_path)) outputs = resolve_output_options(request, config) output_config = config["output"] - output_parts, layer_name = scope_output_identity(output_config["layer_prefix"], request.scope) + + t0 = time.perf_counter() + admin_source = CSAdminSource( + _repo_path(config["sources"]["admin_gpkg"]), + table_name=config["sources"]["admin_layer"], + ) + ( + admin_selection, + registration_scope, + output_parts, + layer_name, + ) = resolved_scope_output_identity( + admin_source, + output_config["layer_prefix"], + AdminScope.from_mapping(asdict(request.scope)), + include_geometry=True, + ) + timings["read_admin_seconds"] = round(time.perf_counter() - t0, 3) + output_root = _repo_path(output_config["root"]).joinpath(*output_parts) bundle = OutputBundle( output_root, @@ -924,13 +1070,9 @@ def run_facilities_pipeline( created_facility_indexes = _ensure_facility_indexes(config) timings["ensure_facility_indexes_seconds"] = round(time.perf_counter() - t0, 3) - t0 = time.perf_counter() - admin_source = CSAdminSource(_repo_path(config["sources"]["admin_gpkg"]), table_name=config["sources"]["admin_layer"]) - admin_selection = admin_source.read_scope(AdminScope.from_mapping(asdict(request.scope)), include_geometry=True) admin_rows = admin_selection.rows bounds = _bbox(admin_rows) village_points = _village_points(admin_rows) - timings["read_admin_seconds"] = round(time.perf_counter() - t0, 3) t0 = time.perf_counter() classification = _classification(config) @@ -954,8 +1096,12 @@ def run_facilities_pipeline( village_service_values = village_service.drop(columns=["fid"], errors="ignore") village_service_gdf = admin_rows[["fid", "geometry"]].copy() village_service_gdf["_admin_key"] = village_service_gdf["fid"] - village_service_gdf = village_service_gdf.merge(village_service_values, on="_admin_key", how="left") - village_service_gdf = gpd.GeoDataFrame(village_service_gdf, geometry="geometry", crs=admin_rows.crs) + village_service_gdf = village_service_gdf.merge( + village_service_values, on="_admin_key", how="left" + ) + village_service_gdf = gpd.GeoDataFrame( + village_service_gdf, geometry="geometry", crs=admin_rows.crs + ) status_name, status_outputs = status_column_config(config) if status_name: village_service_gdf[status_name] = [ @@ -967,11 +1113,16 @@ def run_facilities_pipeline( # Order the GeoPackage columns by the classification schema, then append any # remaining columns (title, layer kind) so nothing is silently dropped. present = set(village_service_gdf.columns) - gpkg_value_columns = [column for column in _machine_output_columns(classification) if column in present] + gpkg_value_columns = [ + column + for column in _machine_output_columns(classification) + if column in present + ] gpkg_value_columns.extend( column for column in village_service_gdf.columns - if column not in set(gpkg_value_columns) | {"geometry", "_admin_key", status_name} + if column + not in set(gpkg_value_columns) | {"geometry", "_admin_key", status_name} ) if status_name and {"gpkg", "geoserver"} & status_outputs: gpkg_value_columns.insert(0, status_name) @@ -1024,7 +1175,9 @@ def run_facilities_pipeline( if outputs.gpkg or (request.publish.sync_to_geoserver and outputs.geoserver): # The GPKG table name becomes the GeoServer feature-type name, so it # must be the scoped layer name rather than a generic table name. - paths["gpkg_path"] = bundle.write_gpkg({layer_name: village_service_output_gdf}).as_posix() + paths["gpkg_path"] = bundle.write_gpkg( + {layer_name: village_service_output_gdf} + ).as_posix() paths["facility_points_gpkg_path"] = bundle.write_gpkg( { tehsil_facilities_layer: tehsil_facilities, @@ -1108,7 +1261,11 @@ def run_facilities_pipeline( layer_name, result, column_dictionary( - pd.DataFrame(village_service_output_gdf.drop(columns=["geometry"], errors="ignore")), + pd.DataFrame( + village_service_output_gdf.drop( + columns=["geometry"], errors="ignore" + ) + ), describe_machine, rename_machine, ), @@ -1128,39 +1285,55 @@ def run_facilities_pipeline( "readme_path": result.get("readme_path"), }, "geoserver": { - "status": geoserver.get("status") if isinstance(geoserver, Mapping) else "not_requested", + "status": ( + geoserver.get("status") + if isinstance(geoserver, Mapping) + else "not_requested" + ), "layers": published_layers, }, } ).as_posix() if request.publish.register_layers and published_layers: - common_misc = { - "source_facilities_gpkg": config["sources"]["facilities_gpkg"], - "gpkg_path": result.get("gpkg_path"), - "facility_points_gpkg_path": result.get("facility_points_gpkg_path"), - "links_path": result.get("links_path"), - "output_dir": bundle.path.as_posix(), - "village_rows": result.get("village_rows"), - "nearest_rows": result.get("nearest_rows"), - "inventory_rows": result.get("inventory_rows"), - } + state = registration_scope.state_name + district = registration_scope.district_name + block = registration_scope.tehsil_name registrations: dict[str, dict[str, Any]] = {} for role, published in published_layers.items(): - registrations[role] = register_layer( - dataset_name=( - output_config.get("dataset_name", "Facilities Proximity") - if role == "village_properties" - else output_config.get("points_dataset_name", "Facilities Points") - ), + dataset_name = ( + output_config.get("dataset_name", "Facilities Proximity") + if role == "village_properties" + else output_config.get("points_dataset_name", "Facilities Proximity") + ) + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, layer_name=published["layer_name"], - scope=request.scope, - workspace=published["workspace"], - geoserver_url=published["wfs_url"], + asset_id="not applicable: local compute GeoServer layer", + dataset_name="Facilities Proximity", algorithm=ALGORITHM, algorithm_version=ALGORITHM_VERSION, - misc={**common_misc, "output_role": role}, - overwrite=request.publish.overwrite, + misc={"is_generated_locally": True}, + is_override=request.publish.overwrite, ) + if layer_id is None: + raise RuntimeError( + f"Database registration failed for layer {published['layer_name']!r}." + ) + if ( + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + is None + ): + raise RuntimeError( + f"GeoServer sync status update failed for layer ID {layer_id}." + ) + registrations[role] = { + "ok": True, + "status": "registered", + "dataset": dataset_name, + "layer_id": layer_id, + } result["layer_registrations"] = registrations result["layer_registration"] = registrations.get("village_properties") result["layer_id"] = (result.get("layer_registration") or {}).get("layer_id") @@ -1175,16 +1348,28 @@ def run_facilities_pipeline( "config_path": str(config_path), "outputs": { "village_properties": frame_profile( - pd.DataFrame(village_service_output_gdf.drop(columns=["geometry"], errors="ignore")), + pd.DataFrame( + village_service_output_gdf.drop( + columns=["geometry"], errors="ignore" + ) + ), describe_machine, rename_machine, ), "tehsil_facility_collection": frame_profile( - pd.DataFrame(tehsil_facilities.drop(columns=["geometry"], errors="ignore")), + pd.DataFrame( + tehsil_facilities.drop( + columns=["geometry"], errors="ignore" + ) + ), _point_column_describer, ), "village_nearest_facility_collection": frame_profile( - pd.DataFrame(village_nearest_facilities.drop(columns=["geometry"], errors="ignore")), + pd.DataFrame( + village_nearest_facilities.drop( + columns=["geometry"], errors="ignore" + ) + ), _point_column_describer, ), }, @@ -1206,7 +1391,18 @@ def run_facilities_request(payload: Mapping[str, Any]) -> dict[str, Any]: @app.task(bind=True, max_retries=3, default_retry_delay=60) -def generate_facilities_proximity_task(self, payload: Mapping[str, Any]): +def generate_facilities_proximity_task( + self, + state: str | None = None, + district: str | None = None, + block: str | None = None, + payload: Mapping[str, Any] | None = None, +): + if payload is None: + payload = api_request_payload( + {"state": state, "district": district, "block": block}, + overwrite=True, + ) return run_facilities_request(payload) @@ -1219,7 +1415,9 @@ def main() -> None: args = parser.parse_args() if not (args.state and args.district and args.tehsil): parser.error("--state, --district, and --tehsil are required") - request = _cli_request(args.state, args.district, args.tehsil, not args.no_geoserver) + request = _cli_request( + args.state, args.district, args.tehsil, not args.no_geoserver + ) result = run_facilities_pipeline(request) print(json.dumps(result, indent=2, default=str)) diff --git a/computing/misc/facilities_proximity_local_compute.py b/computing/misc/facilities_proximity_local_compute.py new file mode 100644 index 00000000..7cd4225d --- /dev/null +++ b/computing/misc/facilities_proximity_local_compute.py @@ -0,0 +1,159 @@ +import os +import geopandas as gpd + +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) +from computing.local_compute_helper import ( + PROJECT_ROOT, + build_output_vector_path, + load_precomputed_panchayat, + read_validated_vector_file, + write_vector_output, + validate_geometry, + read_geojson_with_string_coords, +) +from computing.config_loader import ( + PAN_INDIA_FACILITIES_PATH, + LOCAL_FACILITIES_OUTPUT, +) + +GEOSERVER_WORKSPACE = "facilities_proximity" + + +def _compute_proximity_for_panchayat(panchayat_gdf, facilities_gdf): + if facilities_gdf.empty: + return facilities_gdf + + outer_boundary = panchayat_gdf.geometry.unary_union + + # Keep facilities that intersect the boundary, geometries unchanged + facilities_in_roi = gpd.clip(facilities_gdf, outer_boundary).copy() + + # Final cleanup + facilities_in_roi = facilities_in_roi[~facilities_in_roi.geometry.is_empty] + facilities_in_roi = facilities_in_roi[facilities_in_roi.geometry.is_valid] + facilities_in_roi = facilities_in_roi[facilities_in_roi.geometry.notna()] + + # Rename NAME to censusname + if "NAME" in facilities_in_roi.columns: + facilities_in_roi = facilities_in_roi.rename(columns={"NAME": "censusname"}) + + # Add state, district, tehsil from panchayat_gdf + for col in ["state", "district", "tehsil"]: + if col in panchayat_gdf.columns: + # Assign the value for the tehsil (taking the first valid row) + first_val = panchayat_gdf[col].dropna().iloc[0] if not panchayat_gdf[col].dropna().empty else None + facilities_in_roi[col] = first_val + + return facilities_in_roi + + +@app.task(bind=True) +def generate_facilities_proximity_local( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + gee_account_id=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if state and district and block: + layer_name = f"facilities_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}" + panchayat_gdf, panchayat_source = load_precomputed_panchayat( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Panchayat boundary source: {panchayat_source}") + else: + if not roi_path or not asset_suffix: + raise ValueError("ROI path and asset_suffix are required for custom runs.") + layer_name = f"facilities_{valid_gee_text(asset_suffix).lower()}" + panchayat_gdf = read_validated_vector_file( + roi_path, f"Invalid ROI file: {roi_path}" + ) + print(f"ROI source: {roi_path}") + + if not os.path.exists(PAN_INDIA_FACILITIES_PATH): + raise FileNotFoundError( + f"PAN INDIA Facilities file not found at {PAN_INDIA_FACILITIES_PATH}" + ) + + print("Loading Facilities data overlapping ROI...") + + # Ensure mask is in EPSG:4326 since the source GeoJSON is in EPSG:4326 + if panchayat_gdf.crs and panchayat_gdf.crs.to_epsg() != 4326: + mask_gdf = panchayat_gdf.to_crs(epsg=4326) + else: + mask_gdf = panchayat_gdf + + facilities_gdf = read_geojson_with_string_coords( + PAN_INDIA_FACILITIES_PATH, mask_gdf=mask_gdf + ) + facilities_gdf = validate_geometry(facilities_gdf) + + if facilities_gdf.empty: + print( + "Warning: PAN INDIA Facilities file has no valid geometries overlapping ROI" + ) + print(f"Loaded {len(facilities_gdf)} Facilities features") + + result_gdf = _compute_proximity_for_panchayat( + panchayat_gdf=panchayat_gdf, + facilities_gdf=facilities_gdf, + ) + print(f"Final valid Facilities features after spatial filter: {len(result_gdf)}") + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_FACILITIES_OUTPUT, + ) + + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local Facilities vector: {asset_id}") + + layer_at_geoserver = False + + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer response: {geoserver_response}") + if geoserver_response and geoserver_response.get("status_code") in (200, 201): + layer_at_geoserver = True + + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Facilities Proximity", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for Facilities vector") + + return layer_at_geoserver \ No newline at end of file diff --git a/computing/misc/factory_csr_local_compute.py b/computing/misc/factory_csr_local_compute.py new file mode 100644 index 00000000..bf812ff6 --- /dev/null +++ b/computing/misc/factory_csr_local_compute.py @@ -0,0 +1,176 @@ +import os +import geopandas as gpd + +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) +from computing.local_compute_helper import ( + PROJECT_ROOT, + build_output_vector_path, + load_precomputed_watersheds, + read_validated_vector_file, + write_vector_output, + validate_geometry, +) +from computing.config_loader import ( + PAN_INDIA_FACTORY_CSR_PATH, + LOCAL_FACTORY_CSR_OUTPUT, +) + +GEOSERVER_WORKSPACE = "factory_csr" + + +def _compute_factory_csr_for_watersheds(watersheds_gdf, factory_gdf): + """ + Spatially joins Factory CSR features with watershed polygons. + Equivalent to the GEE Join.saveFirst() with spatial intersection. + """ + if factory_gdf.empty: + return factory_gdf + + if watersheds_gdf.crs and factory_gdf.crs and watersheds_gdf.crs != factory_gdf.crs: + factory_gdf = factory_gdf.to_crs(watersheds_gdf.crs) + + # We only need the 'uid' from watersheds + target_watersheds = watersheds_gdf[["uid", "geometry"]].copy() + + joined_gdf = gpd.sjoin( + factory_gdf, + target_watersheds, + how="inner", + predicate="intersects" + ) + + # To mimic ee.Join.saveFirst(), drop duplicates based on the original feature index + joined_gdf = joined_gdf[~joined_gdf.index.duplicated(keep="first")] + + if "index_right" in joined_gdf.columns: + joined_gdf = joined_gdf.drop(columns=["index_right"]) + + rename_map = {} + for col in joined_gdf.columns: + cl = col.lower().strip() + if cl in ["company_na", "company na", "company name", "company_name"]: rename_map[col] = "COMPANY NA" + elif cl in ["location n", "location_n", "location t", "location_t", "location type", "location_type"]: rename_map[col] = "LOCATION T" + elif cl == "address": rename_map[col] = "ADDRESS" + elif cl in ["level 1", "level_1"]: rename_map[col] = "LEVEL 1" + elif cl in ["level 2", "level_2"]: rename_map[col] = "LEVEL 2" + elif cl in ["level 3", "level_3"]: rename_map[col] = "LEVEL 3" + elif cl == "uuid": rename_map[col] = "UUID" + elif col == "" or "unnamed" in cl: rename_map[col] = "Unnamed_ 9" + elif cl == "lat": rename_map[col] = "LAT" + elif cl in ["lng", "lon", "long", "longitude"]: rename_map[col] = "LNG" + + joined_gdf = joined_gdf.rename(columns=rename_map) + + if "LAT" not in joined_gdf.columns and not joined_gdf.empty: + joined_gdf["LAT"] = joined_gdf.geometry.y + if "LNG" not in joined_gdf.columns and not joined_gdf.empty: + joined_gdf["LNG"] = joined_gdf.geometry.x + + target_cols = [ + "ADDRESS", "COMPANY NA", "LAT", "LEVEL 1", "LEVEL 2", + "LEVEL 3", "LNG", "LOCATION T", "UUID", "Unnamed_ 9", + "uid", "geometry" + ] + cols_to_keep = [col for col in target_cols if col in joined_gdf.columns] + joined_gdf = joined_gdf[cols_to_keep] + + return joined_gdf + + +@app.task(bind=True) +def generate_factory_csr_data_local( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + gee_account_id=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if state and district and block: + layer_name = f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_factory_csr" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + if not roi_path or not asset_suffix: + raise ValueError("ROI path and asset_suffix are required for custom runs.") + layer_name = f"{valid_gee_text(asset_suffix).lower()}_factory_csr" + watersheds_gdf = read_validated_vector_file(roi_path, f"Invalid ROI file: {roi_path}") + print(f"ROI source: {roi_path}") + + if not os.path.exists(PAN_INDIA_FACTORY_CSR_PATH): + raise FileNotFoundError(f"PAN INDIA Factory CSR file not found at {PAN_INDIA_FACTORY_CSR_PATH}") + + print("Loading Factory CSR data overlapping ROI...") + factory_gdf = gpd.read_file(PAN_INDIA_FACTORY_CSR_PATH, mask=watersheds_gdf) + factory_gdf = validate_geometry(factory_gdf) + if factory_gdf.empty: + print("Warning: PAN INDIA Factory CSR file has no valid geometries overlapping ROI") + print(f"Loaded {len(factory_gdf)} Factory CSR features") + + result_gdf = _compute_factory_csr_for_watersheds( + watersheds_gdf=watersheds_gdf, + factory_gdf=factory_gdf, + ) + print(f"Final valid Factory CSR features after spatial join: {len(result_gdf)}") + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_FACTORY_CSR_OUTPUT, + ) + + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local Factory CSR vector: {asset_id}") + + layer_at_geoserver = False + + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer response: {geoserver_response}") + if geoserver_response and geoserver_response.get("status_code") in (200, 201): + layer_at_geoserver = True + + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Factory CSR", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for Factory CSR vector") + + return layer_at_geoserver + + + diff --git a/computing/misc/green_credit_local_compute.py b/computing/misc/green_credit_local_compute.py new file mode 100644 index 00000000..3c92f2fa --- /dev/null +++ b/computing/misc/green_credit_local_compute.py @@ -0,0 +1,147 @@ +import os +import geopandas as gpd + +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) +from computing.local_compute_helper import ( + PROJECT_ROOT, + build_output_vector_path, + load_precomputed_watersheds, + read_validated_vector_file, + write_vector_output, + validate_geometry, +) +from computing.config_loader import ( + PAN_INDIA_GREEN_CREDIT_PATH, + LOCAL_GREEN_CREDIT_OUTPUT, +) + +GEOSERVER_WORKSPACE = "green_credit" + + +def _compute_green_credit_for_watersheds(watersheds_gdf, green_credit_gdf): + """ + Spatially joins Green Credit features with watershed polygons. + Equivalent to the GEE Join.saveFirst() with spatial intersection. + """ + if green_credit_gdf.empty: + return green_credit_gdf + + if watersheds_gdf.crs and green_credit_gdf.crs and watersheds_gdf.crs != green_credit_gdf.crs: + green_credit_gdf = green_credit_gdf.to_crs(watersheds_gdf.crs) + + # We only need the 'uid' from watersheds + target_watersheds = watersheds_gdf[["uid", "geometry"]].copy() + + joined_gdf = gpd.sjoin( + green_credit_gdf, + target_watersheds, + how="inner", + predicate="intersects" + ) + + # To mimic ee.Join.saveFirst(), drop duplicates based on the original feature index + joined_gdf = joined_gdf[~joined_gdf.index.duplicated(keep="first")] + + if "index_right" in joined_gdf.columns: + joined_gdf = joined_gdf.drop(columns=["index_right"]) + + return joined_gdf + + +@app.task(bind=True) +def generate_green_credit_data_local( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + gee_account_id=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if state and district and block: + layer_name = f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_green_credit" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + if not roi_path or not asset_suffix: + raise ValueError("ROI path and asset_suffix are required for custom runs.") + layer_name = f"{valid_gee_text(asset_suffix).lower()}_green_credit" + watersheds_gdf = read_validated_vector_file(roi_path, f"Invalid ROI file: {roi_path}") + print(f"ROI source: {roi_path}") + + if not os.path.exists(PAN_INDIA_GREEN_CREDIT_PATH): + raise FileNotFoundError(f"PAN INDIA Green Credit file not found at {PAN_INDIA_GREEN_CREDIT_PATH}") + + print("Loading Green Credit data overlapping ROI...") + green_credit_gdf = gpd.read_file(PAN_INDIA_GREEN_CREDIT_PATH, mask=watersheds_gdf) + green_credit_gdf = validate_geometry(green_credit_gdf) + if green_credit_gdf.empty: + print("Warning: PAN INDIA Green Credit file has no valid geometries overlapping ROI") + print(f"Loaded {len(green_credit_gdf)} Green Credit features") + + result_gdf = _compute_green_credit_for_watersheds( + watersheds_gdf=watersheds_gdf, + green_credit_gdf=green_credit_gdf, + ) + print(f"Final valid Green Credit features after spatial join: {len(result_gdf)}") + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_GREEN_CREDIT_OUTPUT, + ) + + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local Green Credit vector: {asset_id}") + + layer_at_geoserver = False + + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer response: {geoserver_response}") + if geoserver_response and geoserver_response.get("status_code") in (200, 201): + layer_at_geoserver = True + + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Green Credit", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for Green Credit vector") + + return layer_at_geoserver + + + diff --git a/computing/misc/lcw_conflict_local_compute.py b/computing/misc/lcw_conflict_local_compute.py new file mode 100644 index 00000000..75aea6c4 --- /dev/null +++ b/computing/misc/lcw_conflict_local_compute.py @@ -0,0 +1,147 @@ +import os +import geopandas as gpd + +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) +from computing.local_compute_helper import ( + PROJECT_ROOT, + build_output_vector_path, + load_precomputed_watersheds, + read_validated_vector_file, + write_vector_output, + validate_geometry, +) +from computing.config_loader import ( + PAN_INDIA_LCW_PATH, + LOCAL_LCW_OUTPUT, +) + +GEOSERVER_WORKSPACE = "lcw" + + +def _compute_lcw_conflict_for_watersheds(watersheds_gdf, lcw_gdf): + """ + Spatially joins LCW conflict features with watershed polygons. + Equivalent to the GEE Join.saveFirst() with spatial intersection. + """ + if lcw_gdf.empty: + return lcw_gdf + + if watersheds_gdf.crs and lcw_gdf.crs and watersheds_gdf.crs != lcw_gdf.crs: + lcw_gdf = lcw_gdf.to_crs(watersheds_gdf.crs) + + # We only need the 'uid' from watersheds + target_watersheds = watersheds_gdf[["uid", "geometry"]].copy() + + joined_gdf = gpd.sjoin( + lcw_gdf, + target_watersheds, + how="inner", + predicate="intersects" + ) + + # To mimic ee.Join.saveFirst(), drop duplicates based on the original LCW feature index + joined_gdf = joined_gdf[~joined_gdf.index.duplicated(keep="first")] + + if "index_right" in joined_gdf.columns: + joined_gdf = joined_gdf.drop(columns=["index_right"]) + + return joined_gdf + + +@app.task(bind=True) +def generate_lcw_conflict_data_local( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + gee_account_id=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if state and district and block: + layer_name = f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_lcw_conflict" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + if not roi_path or not asset_suffix: + raise ValueError("ROI path and asset_suffix are required for custom runs.") + layer_name = f"{valid_gee_text(asset_suffix).lower()}_lcw_conflict" + watersheds_gdf = read_validated_vector_file(roi_path, f"Invalid ROI file: {roi_path}") + print(f"ROI source: {roi_path}") + + if not os.path.exists(PAN_INDIA_LCW_PATH): + raise FileNotFoundError(f"PAN INDIA LCW conflict file not found at {PAN_INDIA_LCW_PATH}") + + print("Loading LCW conflict data overlapping ROI...") + lcw_gdf = gpd.read_file(PAN_INDIA_LCW_PATH, mask=watersheds_gdf) + lcw_gdf = validate_geometry(lcw_gdf) + if lcw_gdf.empty: + print("Warning: PAN INDIA LCW conflict file has no valid geometries overlapping ROI") + print(f"Loaded {len(lcw_gdf)} LCW features") + + result_gdf = _compute_lcw_conflict_for_watersheds( + watersheds_gdf=watersheds_gdf, + lcw_gdf=lcw_gdf, + ) + print(f"Final valid LCW features after spatial join: {len(result_gdf)}") + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_LCW_OUTPUT, + ) + + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local LCW conflict vector: {asset_id}") + + layer_at_geoserver = False + + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer response: {geoserver_response}") + if geoserver_response and geoserver_response.get("status_code") in (200, 201): + layer_at_geoserver = True + + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="LCW Conflict", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for LCW conflict vector") + + return layer_at_geoserver + + + diff --git a/computing/misc/livestocks/pipeline.py b/computing/misc/livestocks/pipeline.py index d8de86a7..ed374758 100644 --- a/computing/misc/livestocks/pipeline.py +++ b/computing/misc/livestocks/pipeline.py @@ -12,6 +12,7 @@ import pandas as pd from django.conf import settings +from computing.utils import save_layer_info_to_db, update_layer_sync_status from utilities.pipelines import AdminScope, CSAdminSource, StandardRequest, load_config from utilities.pipelines.admin import ( ADMIN_COLUMN_DESCRIPTIONS, @@ -23,11 +24,12 @@ column_dictionary, frame_profile, input_signatures, + resolved_scope_output_identity, slug, stable_hash, utc_now_text, ) -from utilities.pipelines.publish import publish_gpkg_layer, register_layer +from utilities.pipelines.publish import publish_gpkg_layer from utilities.pipelines.schema import ( STATUS_MATCHED, STATUS_NO_DATA, @@ -46,11 +48,11 @@ LIVESTOCK_CENSUS_20_CSV, LIVESTOCK_GEOSERVER_WORKSPACE, ) - +from utilities.pipelines import api_request_payload CONFIG_PATH = Path(__file__).with_name("livestocks_pipeline.yaml") ALGORITHM = "local-livestock-csv-admin-join" -ALGORITHM_VERSION = "2.0" +ALGORITHM_VERSION = "2.1" SOURCE_DEFAULTS = { "admin_gpkg": ADMIN_BOUNDARY_GPKG, "csv": LIVESTOCK_CENSUS_20_CSV, @@ -79,11 +81,9 @@ def _apply_source_defaults(config: Mapping[str, Any]) -> dict[str, Any]: return resolved -def _layer_name(prefix: str, district: str | None, tehsil: str | None) -> str: - return f"{prefix}_{slug(district)}_{slug(tehsil)}".strip("_") - - -def _cli_request(state: str, district: str, tehsil: str, sync_to_geoserver: bool = True) -> StandardRequest: +def _cli_request( + state: str, district: str, tehsil: str, sync_to_geoserver: bool = True +) -> StandardRequest: return StandardRequest.from_mapping( { "scope": { @@ -121,7 +121,9 @@ def _schema(config: Mapping[str, Any]) -> dict[str, Any]: return load_config(_repo_path(path)) if path else {} -def _derive_livestock_metrics(frame: pd.DataFrame, schema: Mapping[str, Any]) -> pd.DataFrame: +def _derive_livestock_metrics( + frame: pd.DataFrame, schema: Mapping[str, Any] +) -> pd.DataFrame: derived = frame.copy() for _, animals in schema.get("livestock", {}).items(): for fields in animals.values(): @@ -129,27 +131,37 @@ def _derive_livestock_metrics(frame: pd.DataFrame, schema: Mapping[str, Any]) -> female = fields.get("female") total = fields.get("total") if male in derived.columns and female in derived.columns and total: - derived[total] = ( - pd.to_numeric(derived[male], errors="coerce").fillna(0) - + pd.to_numeric(derived[female], errors="coerce").fillna(0) - ) + derived[total] = pd.to_numeric(derived[male], errors="coerce").fillna( + 0 + ) + pd.to_numeric(derived[female], errors="coerce").fillna(0) for metric, spec in schema.get("derived_metrics", {}).items(): sources = spec.get("sources", []) if sources and all(source in derived.columns for source in sources): - derived[metric] = sum(pd.to_numeric(derived[source], errors="coerce").fillna(0) for source in sources) + derived[metric] = sum( + pd.to_numeric(derived[source], errors="coerce").fillna(0) + for source in sources + ) return derived -def _validate_livestock(frame: pd.DataFrame, config: Mapping[str, Any]) -> list[ValidationIssue]: +def _validate_livestock( + frame: pd.DataFrame, config: Mapping[str, Any] +) -> list[ValidationIssue]: return validate_numeric_range( frame, - columns=[column for column in config["metrics"]["count_columns"] if column in frame.columns], + columns=[ + column + for column in config["metrics"]["count_columns"] + if column in frame.columns + ], minimum=0, allow_null=True, ) -def _merge_admin_livestock(admin_rows, source_rows: pd.DataFrame, config: Mapping[str, Any]): +def _merge_admin_livestock( + admin_rows, source_rows: pd.DataFrame, config: Mapping[str, Any] +): admin = admin_rows.copy() attrs = source_rows.copy() attrs = attrs.drop(columns=["state_name", "district_name"], errors="ignore") @@ -162,7 +174,9 @@ def _merge_admin_livestock(admin_rows, source_rows: pd.DataFrame, config: Mappin ) -def _ordered_columns(frame: pd.DataFrame, config: Mapping[str, Any], columns: Mapping[str, list[str]]) -> list[str]: +def _ordered_columns( + frame: pd.DataFrame, config: Mapping[str, Any], columns: Mapping[str, list[str]] +) -> list[str]: ordered = [] ordered.extend([col for col in columns["location"] if col not in ordered]) ordered.extend([col for col in columns["metrics"] if col not in ordered]) @@ -170,19 +184,29 @@ def _ordered_columns(frame: pd.DataFrame, config: Mapping[str, Any], columns: Ma return [col for col in ordered if col in frame.columns] -def _focused_frame(frame: pd.DataFrame, value_columns: list[str], status_name: str | None) -> pd.DataFrame: +def _focused_frame( + frame: pd.DataFrame, value_columns: list[str], status_name: str | None +) -> pd.DataFrame: """Return the report CSV frame: admin columns, the status column, then the configured value columns for matched villages.""" - focused = admin_presentation_frame(frame.drop(columns=["geometry"], errors="ignore")) + focused = admin_presentation_frame( + frame.drop(columns=["geometry"], errors="ignore") + ) source = frame.set_index("fid", drop=False) if "fid" in frame.columns else frame output_rows: list[dict[str, Any]] = [] for _, admin_row in focused.iterrows(): row = admin_row.to_dict() admin_index = row.get("index") - values = source.loc[admin_index] if admin_index in source.index else pd.Series(dtype=object) + values = ( + source.loc[admin_index] + if admin_index in source.index + else pd.Series(dtype=object) + ) has_village_id = pd.notna(row.get("village_id")) - has_livestock = pd.notna(values.get("village_code")) if not values.empty else False + has_livestock = ( + pd.notna(values.get("village_code")) if not values.empty else False + ) status = STATUS_MATCHED if not has_village_id: status = STATUS_NO_VILLAGE_ID @@ -222,18 +246,26 @@ def _column_describer(schema: Mapping[str, Any], config: Mapping[str, Any]): f"20th Livestock Census (2019), {group_label} group." ) if fields.get("female"): - descriptions[fields["female"]] = f"Female {animal} count for the village, 20th Livestock Census (2019)." + descriptions[fields["female"]] = ( + f"Female {animal} count for the village, 20th Livestock Census (2019)." + ) if fields.get("male"): - descriptions[fields["male"]] = f"Male {animal} count for the village, 20th Livestock Census (2019)." + descriptions[fields["male"]] = ( + f"Male {animal} count for the village, 20th Livestock Census (2019)." + ) for metric, spec in (schema.get("derived_metrics") or {}).items(): sources = ", ".join(spec.get("sources", [])) label = spec.get("label", metric.replace("_", " ").title()) descriptions[metric] = f"{label}: sum of {sources}." - descriptions["village_code"] = "Census village code used to join livestock census records." + descriptions["village_code"] = ( + "Census village code used to join livestock census records." + ) return descriptions -def _overview(frame: pd.DataFrame, group_columns: list[str], config: Mapping[str, Any]) -> pd.DataFrame: +def _overview( + frame: pd.DataFrame, group_columns: list[str], config: Mapping[str, Any] +) -> pd.DataFrame: groups = [col for col in group_columns if col in frame.columns] if not groups: return pd.DataFrame() @@ -244,10 +276,14 @@ def _overview(frame: pd.DataFrame, group_columns: list[str], config: Mapping[str keys = (keys,) row = dict(zip(groups, keys)) row["admin_village_rows"] = int(len(group)) - row["matched_livestock_rows"] = int(group["village_code"].notna().sum()) if "village_code" in group else 0 + row["matched_livestock_rows"] = ( + int(group["village_code"].notna().sum()) if "village_code" in group else 0 + ) for metric in metrics: if metric in group.columns: - row[f"{metric}_sum"] = int(pd.to_numeric(group[metric], errors="coerce").fillna(0).sum()) + row[f"{metric}_sum"] = int( + pd.to_numeric(group[metric], errors="coerce").fillna(0).sum() + ) rows.append(row) return pd.DataFrame(rows) @@ -260,8 +296,12 @@ def _column_reference_lines(column_entries: list[Mapping[str, Any]]) -> list[str "| --- | --- | --- |", ] for entry in column_entries: - description = str(entry.get("description") or "").replace("|", "\\|").replace("\n", " ") - lines.append(f"| `{entry['column']}` | {entry.get('datatype', '')} | {description} |") + description = ( + str(entry.get("description") or "").replace("|", "\\|").replace("\n", " ") + ) + lines.append( + f"| `{entry['column']}` | {entry.get('datatype', '')} | {description} |" + ) lines.append("") return lines @@ -324,7 +364,9 @@ def _readme_lines( return lines -def _cache_input_signatures(config: Mapping[str, Any], config_path: str | Path) -> dict[str, dict[str, Any]]: +def _cache_input_signatures( + config: Mapping[str, Any], config_path: str | Path +) -> dict[str, dict[str, Any]]: sources = config.get("sources", {}) paths: dict[str, str | Path] = { "pipeline_config": _repo_path(config_path), @@ -350,7 +392,9 @@ def _cache_key(request: StandardRequest, outputs: OutputOptions) -> str: ) -def _required_result_paths(outputs: OutputOptions, request: StandardRequest) -> tuple[str, ...]: +def _required_result_paths( + outputs: OutputOptions, request: StandardRequest +) -> tuple[str, ...]: required: list[str] = ["links_path"] if outputs.metadata: required.append("run_metadata_path") @@ -373,8 +417,27 @@ def run_livestocks_pipeline( schema = _schema(config) columns = _source_columns(config) output_config = config["output"] - layer_name = _layer_name(output_config["layer_prefix"], request.scope.district_name, request.scope.tehsil_name) - output_root = _repo_path(output_config["root"]) / slug(request.scope.state_name) / slug(request.scope.district_name) / slug(request.scope.tehsil_name) + + t0 = time.perf_counter() + admin_source = CSAdminSource( + _repo_path(config["sources"]["admin_gpkg"]), + table_name=config["sources"]["admin_layer"], + ) + include_geometry = outputs.gpkg or request.publish.sync_to_geoserver + ( + admin_selection, + registration_scope, + output_parts, + layer_name, + ) = resolved_scope_output_identity( + admin_source, + output_config["layer_prefix"], + AdminScope.from_mapping(asdict(request.scope)), + include_geometry=include_geometry, + ) + timings["read_admin_seconds"] = round(time.perf_counter() - t0, 3) + + output_root = _repo_path(output_config["root"]).joinpath(*output_parts) bundle = OutputBundle( output_root, layer_name, @@ -393,16 +456,12 @@ def run_livestocks_pipeline( cached["cache_hit"] = True return cached - t0 = time.perf_counter() - admin_source = CSAdminSource(_repo_path(config["sources"]["admin_gpkg"]), table_name=config["sources"]["admin_layer"]) - include_geometry = outputs.gpkg or request.publish.sync_to_geoserver - admin_selection = admin_source.read_scope(AdminScope.from_mapping(asdict(request.scope)), include_geometry=include_geometry) - timings["read_admin_seconds"] = round(time.perf_counter() - t0, 3) - t0 = time.perf_counter() sidecar = _sidecar(config) sidecar_status = sidecar.materialize() - source_rows = sidecar.fetch_by_values(config["keys"]["source_join_key"], admin_selection.pc11_village_ids) + source_rows = sidecar.fetch_by_values( + config["keys"]["source_join_key"], admin_selection.pc11_village_ids + ) timings["read_livestock_seconds"] = round(time.perf_counter() - t0, 3) t0 = time.perf_counter() @@ -411,20 +470,36 @@ def run_livestocks_pipeline( joined = _derive_livestock_metrics(joined, schema) status_name, status_outputs = status_column_config(config) if status_name: - village_codes = joined["village_code"] if "village_code" in joined.columns else pd.Series([None] * len(joined), index=joined.index) + village_codes = ( + joined["village_code"] + if "village_code" in joined.columns + else pd.Series([None] * len(joined), index=joined.index) + ) joined[status_name] = [ - STATUS_NO_VILLAGE_ID - if pd.isna(village_id) - else (STATUS_MATCHED if pd.notna(village_code) else STATUS_NO_DATA) + ( + STATUS_NO_VILLAGE_ID + if pd.isna(village_id) + else (STATUS_MATCHED if pd.notna(village_code) else STATUS_NO_DATA) + ) for village_id, village_code in zip(joined["village_id"], village_codes) ] ordered = _ordered_columns(joined, config, columns) - villages_frame = admin_output_frame(joined.drop(columns=["geometry"], errors="ignore"), value_columns=ordered) - matched_rows = int(villages_frame["village_code"].notna().sum()) if "village_code" in villages_frame else 0 - gpkg_value_columns = [column for column in schema.get("gpkg_columns", []) if column in joined.columns] or ordered + villages_frame = admin_output_frame( + joined.drop(columns=["geometry"], errors="ignore"), value_columns=ordered + ) + matched_rows = ( + int(villages_frame["village_code"].notna().sum()) + if "village_code" in villages_frame + else 0 + ) + gpkg_value_columns = [ + column for column in schema.get("gpkg_columns", []) if column in joined.columns + ] or ordered if status_name and {"gpkg", "geoserver"} & status_outputs: gpkg_value_columns = [status_name, *gpkg_value_columns] - gpkg_frame = admin_output_frame(joined, value_columns=gpkg_value_columns, include_geometry=True) + gpkg_frame = admin_output_frame( + joined, value_columns=gpkg_value_columns, include_geometry=True + ) gpkg_frame = normalize_unicode_frame(gpkg_frame) describe = _column_describer(schema, config) timings["build_outputs_seconds"] = round(time.perf_counter() - t0, 3) @@ -445,7 +520,9 @@ def run_livestocks_pipeline( "layer_name": layer_name, "rows": int(len(villages_frame)), "matched_rows": matched_rows, - "join_coverage": round(matched_rows / len(villages_frame), 6) if len(villages_frame) else 0, + "join_coverage": ( + round(matched_rows / len(villages_frame), 6) if len(villages_frame) else 0 + ), "validation_issues": [asdict(issue) for issue in validation_issues], "sidecar": sidecar_status, "admin_created_indexes": admin_selection.created_indexes, @@ -486,29 +563,6 @@ def run_livestocks_pipeline( geoserver = asdict(geoserver_result) geoserver["ok"] = True geoserver["status"] = "published" - if request.publish.register_layers: - result["layer_registration"] = register_layer( - dataset_name=output_config.get("dataset_name", "Livestock Census"), - layer_name=layer_name, - scope=request.scope, - workspace=geoserver_workspace, - geoserver_url=geoserver.get("wfs_url"), - algorithm=ALGORITHM, - algorithm_version=ALGORITHM_VERSION, - misc={ - "source_csv": config["sources"]["csv"], - "gpkg_path": result.get("gpkg_path"), - "links_path": result.get("links_path"), - "output_dir": bundle.path.as_posix(), - "geoserver_layer_name": layer_name, - "geoserver_url": geoserver.get("wfs_url"), - "rows": result.get("rows"), - "matched_rows": result.get("matched_rows"), - "join_coverage": result.get("join_coverage"), - }, - overwrite=request.publish.overwrite, - ) - result["layer_id"] = (result["layer_registration"] or {}).get("layer_id") except Exception as exc: geoserver = { "ok": False, @@ -520,6 +574,38 @@ def run_livestocks_pipeline( } timings["publish_geoserver_seconds"] = round(time.perf_counter() - t0, 3) result["geoserver"] = geoserver + if request.publish.register_layers and geoserver and geoserver.get("ok"): + state = registration_scope.state_name + district = registration_scope.district_name + block = registration_scope.tehsil_name + dataset_name = output_config.get("dataset_name", "Livestock Census") + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id="not applicable: local compute GeoServer layer", + dataset_name=dataset_name, + algorithm=ALGORITHM, + algorithm_version=ALGORITHM_VERSION, + misc={"is_generated_locally": True}, + is_override=request.publish.overwrite, + ) + if layer_id is None: + raise RuntimeError( + f"Database registration failed for layer {layer_name!r}." + ) + if update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) is None: + raise RuntimeError( + f"GeoServer sync status update failed for layer ID {layer_id}." + ) + result["layer_id"] = layer_id + result["layer_registration"] = { + "ok": True, + "status": "registered", + "dataset": dataset_name, + "layer_id": layer_id, + } if outputs.readme: result["readme_path"] = bundle.write_readme( _readme_lines( @@ -531,7 +617,9 @@ def run_livestocks_pipeline( issues=validation_issues, geoserver=geoserver, column_entries=column_dictionary( - pd.DataFrame(gpkg_frame.drop(columns=["geometry"], errors="ignore")), + pd.DataFrame( + gpkg_frame.drop(columns=["geometry"], errors="ignore") + ), describe, ), ) @@ -557,7 +645,9 @@ def run_livestocks_pipeline( "config_path": str(config_path), "outputs": { "villages": frame_profile( - pd.DataFrame(gpkg_frame.drop(columns=["geometry"], errors="ignore")), + pd.DataFrame( + gpkg_frame.drop(columns=["geometry"], errors="ignore") + ), describe, ), }, @@ -579,9 +669,19 @@ def run_livestocks_request(payload: Mapping[str, Any]) -> dict[str, Any]: @app.task(bind=True) -def generate_livestocks_layer_task(self, payload: Mapping[str, Any]) -> dict[str, Any]: +def generate_livestocks_layer_task( + self, + state: str | None = None, + district: str | None = None, + block: str | None = None, + payload: Mapping[str, Any] | None = None, +) -> dict[str, Any]: """Generate livestock census outputs for a standard request payload.""" - + if payload is None: + payload = api_request_payload( + {"state": state, "district": district, "block": block}, + overwrite=True, + ) return run_livestocks_request(payload) diff --git a/computing/misc/livestocks_local_compute.py b/computing/misc/livestocks_local_compute.py new file mode 100644 index 00000000..37ed5764 --- /dev/null +++ b/computing/misc/livestocks_local_compute.py @@ -0,0 +1,159 @@ +import os +import geopandas as gpd + +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) +from computing.local_compute_helper import ( + PROJECT_ROOT, + build_output_vector_path, + load_precomputed_panchayat, + read_validated_vector_file, + write_vector_output, + validate_geometry, +) +from computing.config_loader import ( + PAN_INDIA_LIVESTOCKS, + LOCAL_LIVESTOCKS_OUTPUT, +) + +GEOSERVER_WORKSPACE = "livestocks" + +INTEGER_COLUMNS = ( + "pc11_village_id", + "pc11_state_id", + "pc11_district_id", + "pc11_subdistrict_id", + "cattle_male", + "cattle_female", + "cattle_total", + "buffalo_male", + "buffalo_female", + "buffalo_total", + "sheep_male", + "sheep_female", + "sheep_total", + "goat_male", + "goat_female", + "goat_total", + "pig_male", + "pig_female", + "pig_total", +) + +def _coerce_nullable_integer_columns(gdf): + """ + Ensure the livestock columns are the correct integer types. + """ + for column in INTEGER_COLUMNS: + if column in gdf.columns: + gdf[column] = gdf[column].astype("Int64") + return gdf + +def _compute_livestocks_for_panchayat(panchayat_gdf, livestocks_gdf): + if livestocks_gdf.empty: + return livestocks_gdf + + outer_boundary = panchayat_gdf.geometry.unary_union + + # Clip Antyodaya geometries to the panchayat boundary + livestocks_in_roi = gpd.clip(livestocks_gdf, outer_boundary).copy() + livestocks_in_roi = livestocks_in_roi[~livestocks_in_roi.geometry.is_empty] + livestocks_in_roi = livestocks_in_roi[livestocks_in_roi.geometry.is_valid] + livestocks_in_roi = livestocks_in_roi[livestocks_in_roi.geometry.notna()] + + return _coerce_nullable_integer_columns(livestocks_in_roi) + + +@app.task(bind=True) +def generate_livestocks_data_local( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + gee_account_id=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if state and district and block: + layer_name = f"livestocks_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}" + panchayat_gdf, watershed_source = load_precomputed_panchayat( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + else: + if not roi_path or not asset_suffix: + raise ValueError("ROI path and asset_suffix are required for custom runs.") + layer_name = f"livestocks_{valid_gee_text(asset_suffix).lower()}" + panchayat_gdf = read_validated_vector_file(roi_path, f"Invalid ROI file: {roi_path}") + print(f"ROI source: {roi_path}") + + if not os.path.exists(PAN_INDIA_LIVESTOCKS): + raise FileNotFoundError(f"PAN INDIA Livestocks file not found at {PAN_INDIA_LIVESTOCKS}") + + print("Loading Livestocks data overlapping ROI...") + livestocks_gdf = gpd.read_file(PAN_INDIA_LIVESTOCKS, mask=panchayat_gdf) + livestocks_gdf = validate_geometry(livestocks_gdf) + if livestocks_gdf.empty: + print("Warning: PAN INDIA Livestocks file has no valid geometries overlapping ROI") + else: + print(f"Loaded {len(livestocks_gdf)} Livestock features") + + result_gdf = _compute_livestocks_for_panchayat( + panchayat_gdf=panchayat_gdf, + livestocks_gdf=livestocks_gdf, + ) + print(f"Final valid Livestock features after spatial filter: {len(result_gdf)}") + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_LIVESTOCKS_OUTPUT, + ) + + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local Livestock vector: {asset_id}") + + layer_at_geoserver = False + + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer response: {geoserver_response}") + if geoserver_response and geoserver_response.get("status_code") in (200, 201): + layer_at_geoserver = True + + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Livestock Census 2019", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for Livestock vector") + + return layer_at_geoserver \ No newline at end of file diff --git a/computing/misc/mining_data_local_compute.py b/computing/misc/mining_data_local_compute.py new file mode 100644 index 00000000..b5bb9cc5 --- /dev/null +++ b/computing/misc/mining_data_local_compute.py @@ -0,0 +1,147 @@ +import os +import geopandas as gpd + +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) +from computing.local_compute_helper import ( + PROJECT_ROOT, + build_output_vector_path, + load_precomputed_watersheds, + read_validated_vector_file, + write_vector_output, + validate_geometry, +) +from computing.config_loader import ( + PAN_INDIA_MINING_PATH, + LOCAL_MINING_OUTPUT, +) + +GEOSERVER_WORKSPACE = "mining" + + +def _compute_mining_data_for_watersheds(watersheds_gdf, mining_gdf): + """ + Spatially joins Mining features with watershed polygons. + Equivalent to the GEE Join.saveFirst() with spatial intersection. + """ + if mining_gdf.empty: + return mining_gdf + + if watersheds_gdf.crs and mining_gdf.crs and watersheds_gdf.crs != mining_gdf.crs: + mining_gdf = mining_gdf.to_crs(watersheds_gdf.crs) + + # We only need the 'uid' from watersheds + target_watersheds = watersheds_gdf[["uid", "geometry"]].copy() + + joined_gdf = gpd.sjoin( + mining_gdf, + target_watersheds, + how="inner", + predicate="intersects" + ) + + # To mimic ee.Join.saveFirst(), drop duplicates based on the original feature index + joined_gdf = joined_gdf[~joined_gdf.index.duplicated(keep="first")] + + if "index_right" in joined_gdf.columns: + joined_gdf = joined_gdf.drop(columns=["index_right"]) + + return joined_gdf + + +@app.task(bind=True) +def generate_mining_data_local( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + gee_account_id=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if state and district and block: + layer_name = f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_mining" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + if not roi_path or not asset_suffix: + raise ValueError("ROI path and asset_suffix are required for custom runs.") + layer_name = f"{valid_gee_text(asset_suffix).lower()}_mining" + watersheds_gdf = read_validated_vector_file(roi_path, f"Invalid ROI file: {roi_path}") + print(f"ROI source: {roi_path}") + + if not os.path.exists(PAN_INDIA_MINING_PATH): + raise FileNotFoundError(f"PAN INDIA Mining data file not found at {PAN_INDIA_MINING_PATH}") + + print("Loading Mining data overlapping ROI...") + mining_gdf = gpd.read_file(PAN_INDIA_MINING_PATH, mask=watersheds_gdf) + mining_gdf = validate_geometry(mining_gdf) + if mining_gdf.empty: + print("Warning: PAN INDIA Mining data file has no valid geometries overlapping ROI") + print(f"Loaded {len(mining_gdf)} Mining features") + + result_gdf = _compute_mining_data_for_watersheds( + watersheds_gdf=watersheds_gdf, + mining_gdf=mining_gdf, + ) + print(f"Final valid Mining features after spatial join: {len(result_gdf)}") + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_MINING_OUTPUT, + ) + + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local Mining data vector: {asset_id}") + + layer_at_geoserver = False + + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer response: {geoserver_response}") + if geoserver_response and geoserver_response.get("status_code") in (200, 201): + layer_at_geoserver = True + + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Mining", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for Mining data vector") + + return layer_at_geoserver + + + diff --git a/computing/misc/naturaldepression_local_compute.py b/computing/misc/naturaldepression_local_compute.py new file mode 100644 index 00000000..29871f55 --- /dev/null +++ b/computing/misc/naturaldepression_local_compute.py @@ -0,0 +1,119 @@ +import os +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.utils import ( + save_layer_info_to_db, + update_layer_sync_status, +) +from computing.local_compute_helper import ( + PROJECT_ROOT, + build_output_raster_path, + load_precomputed_watersheds, + read_validated_vector_file, + clip_raster_with_roi, + push_local_raster_to_geoserver, +) +from computing.STAC_specs import generate_STAC_layerwise +from computing.config_loader import ( + PAN_INDIA_NATURALDEPRESSION_PATH, + LOCAL_NATURALDEPRESSION_OUTPUT, +) + +GEOSERVER_WORKSPACE = "natural_depression" +NATURAL_DEPRESSION_STYLE_NAME = "natural_depression" + + +@app.task(bind=True) +def generate_natural_depression_data_local( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + gee_account_id=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if state and district and block: + layer_name_base = f"natural_depression_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_raster" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + if not roi_path or not asset_suffix: + raise ValueError("ROI path and asset_suffix are required for custom runs.") + layer_name_base = ( + f"natural_depression_{valid_gee_text(asset_suffix).lower()}_raster" + ) + watersheds_gdf = read_validated_vector_file( + roi_path, f"Invalid ROI file: {roi_path}" + ) + print(f"ROI source: {roi_path}") + + # Raster Processing + raster_layer_name = f"{layer_name_base}" + output_raster_path = build_output_raster_path( + layer_name=raster_layer_name, + output_base_dir=LOCAL_NATURALDEPRESSION_OUTPUT, + state=state, + district=district, + block=block, + ) + + print("Clipping Natural Depression raster...") + clipped_raster_path = clip_raster_with_roi( + roi_gdf=watersheds_gdf, + raster_path=PAN_INDIA_NATURALDEPRESSION_PATH, + output_path=output_raster_path, + raster_label="Natural Depression Raster", + ) + print(f"Saved clipped Natural Depression raster to: {clipped_raster_path}") + + layer_at_geoserver = False + if push_to_geoserver: + push_local_raster_to_geoserver( + file_path=str(clipped_raster_path), + layer_name=raster_layer_name, + workspace=GEOSERVER_WORKSPACE, + style_name=NATURAL_DEPRESSION_STYLE_NAME, + ) + print(f"Pushed raster {raster_layer_name} to GeoServer.") + layer_at_geoserver = True + + if sync_layer_metadata and state and district and block: + raster_layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=raster_layer_name, + asset_id=str(clipped_raster_path), + dataset_name="Natural Depression", + misc={"is_generated_locally": True}, + ) + if raster_layer_id: + update_layer_sync_status(layer_id=raster_layer_id, sync_to_geoserver=True) + print(f"Database record updated for raster layer_id: {raster_layer_id}") + + # STAC Specs for Raster + try: + layer_STAC_generated = generate_STAC_layerwise.generate_raster_stac( + state=state, + district=district, + block=block, + layer_name="natural_depression_raster", + ) + update_layer_sync_status( + layer_id=raster_layer_id, + is_stac_specs_generated=layer_STAC_generated, + ) + print("STAC metadata updated for Natural Depression raster") + except Exception as e: + print(f"Error generating STAC for raster: {e}") + + return layer_at_geoserver diff --git a/computing/misc/nrega_local_compute.py b/computing/misc/nrega_local_compute.py new file mode 100644 index 00000000..41547fff --- /dev/null +++ b/computing/misc/nrega_local_compute.py @@ -0,0 +1,167 @@ +import os +import boto3 +import geopandas as gpd +import pandas as pd +import numpy as np +from io import BytesIO + +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from nrm_app.settings import NREGA_BUCKET, S3_ACCESS_KEY, S3_SECRET_KEY +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) +from computing.local_compute_helper import ( + load_precomputed_watersheds, + read_validated_vector_file, + write_vector_output, + build_output_vector_path, + validate_geometry, +) +from computing.config_loader import NREGA_LOCAL_OUTPUT + +GEOSERVER_WORKSPACE = "nrega_assets" + + +def _compute_nrega_for_watersheds(watersheds_gdf, nrega_gdf): + if nrega_gdf.empty: + return nrega_gdf + + watersheds_gdf = validate_geometry(watersheds_gdf) + nrega_gdf = validate_geometry(nrega_gdf) + + if watersheds_gdf.crs and nrega_gdf.crs and watersheds_gdf.crs != nrega_gdf.crs: + nrega_gdf = nrega_gdf.to_crs(watersheds_gdf.crs) + + outer_boundary = watersheds_gdf.geometry.unary_union + + nrega_in_roi = nrega_gdf[nrega_gdf.intersects(outer_boundary)].copy() + + # Clean column names + cleaned_columns = [] + for i, col in enumerate(nrega_in_roi.columns): + if not str(col).strip(): + cleaned_columns.append(f"col_{i}") + else: + cleaned = str(col).strip().replace(" ", "_").replace(".", "_") + cleaned_columns.append(cleaned) + nrega_in_roi.columns = cleaned_columns + + nrega_in_roi = nrega_in_roi.replace({np.nan: None}) + + for col in nrega_in_roi.columns: + if col != "geometry": + nrega_in_roi[col] = nrega_in_roi[col].map( + lambda value: value.isoformat() + if isinstance(value, pd.Timestamp) + else value + ) + + return nrega_in_roi + + +@app.task(bind=True) +def generate_nrega_data_local( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + gee_account_id=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if state and district and block: + layer_name = f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + if not roi_path or not asset_suffix: + raise ValueError("ROI path and asset_suffix are required for custom runs.") + layer_name = f"{valid_gee_text(asset_suffix).lower()}_nrega" + watersheds_gdf = read_validated_vector_file(roi_path, f"Invalid ROI file: {roi_path}") + print(f"ROI source: {roi_path}") + + s3 = boto3.resource( + "s3", + region_name="ap-south-1", + aws_access_key_id=S3_ACCESS_KEY, + aws_secret_access_key=S3_SECRET_KEY, + ) + + print("Fetching NREGA data from S3...") + # It fetches district level geojson from S3 + if state and district: + key = f"{valid_gee_text(state).upper()}/{valid_gee_text(district).upper()}.geojson" + else: + # Fallback or error if custom ROI does not provide state/district mapping to S3 + print("Warning: Custom ROI run without state/district cannot fetch from S3 reliably.") + return False + + try: + file_obj = s3.Object(NREGA_BUCKET, key).get() + nrega_gdf = gpd.read_file(BytesIO(file_obj["Body"].read())) + except Exception as e: + print("Error while reading NREGA file from S3:", e) + return False + + print(f"Loaded {len(nrega_gdf)} NREGA features from S3") + + result_gdf = _compute_nrega_for_watersheds( + watersheds_gdf=watersheds_gdf, + nrega_gdf=nrega_gdf, + ) + print(f"Final valid NREGA features after spatial filter: {len(result_gdf)}") + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=NREGA_LOCAL_OUTPUT, + ) + + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local NREGA vector: {asset_id}") + + layer_at_geoserver = False + + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer response: {geoserver_response}") + if geoserver_response and geoserver_response.get("status_code") in (200, 201): + layer_at_geoserver = True + + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="NREGA Assets", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for NREGA vector") + + return layer_at_geoserver diff --git a/computing/misc/restoration_opportunity_local_compute.py b/computing/misc/restoration_opportunity_local_compute.py new file mode 100644 index 00000000..a3353373 --- /dev/null +++ b/computing/misc/restoration_opportunity_local_compute.py @@ -0,0 +1,274 @@ +import os +from pathlib import Path + +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) +from computing.local_compute_helper import ( + PROJECT_ROOT, + load_precomputed_watersheds, + read_validated_vector_file, + clip_raster_with_roi, + push_local_raster_to_geoserver, + compute_categorical_raster_areas_for_watersheds, + build_output_raster_path, + build_output_vector_path, + write_vector_output, +) +from computing.STAC_specs import generate_STAC_layerwise + + +from computing.config_loader import ( + PAN_INDIA_RESTORATION_PATH, + LOCAL_RESTORATION_OUTPUT, +) + +GEOSERVER_WORKSPACE = "restoration" +RESTORATION_STYLE_NAME = "restoration_style" + + +RESTORATION_CLASSES = [ + {"value": 0, "label": "Excluded A"}, + {"value": 1, "label": "Mosaic Res"}, + {"value": 2, "label": "Wide-scale"}, + {"value": 3, "label": "Protection"}, +] + + +def run_raster_restoration_local( + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if state and district and block: + layer_name = f"restoration_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_raster" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Loaded watersheds from {watershed_source}") + else: + if not roi_path or not asset_suffix: + raise ValueError("ROI path and asset_suffix are required for custom runs.") + layer_name = f"{asset_suffix}_restoration_raster".lower() + watersheds_gdf = read_validated_vector_file(roi_path, f"Invalid ROI file: {roi_path}") + + # ========================================== + # 1. Raster Processing + # ========================================== + output_raster_path = build_output_raster_path( + layer_name=layer_name, + output_base_dir=LOCAL_RESTORATION_OUTPUT, + state=state, + district=district, + block=block, + ) + + print("Clipping restoration raster...") + clipped_raster_path = clip_raster_with_roi( + roi_gdf=watersheds_gdf, + raster_path=PAN_INDIA_RESTORATION_PATH, + output_path=output_raster_path, + raster_label="WRI Restoration Raster", + ) + print(f"Saved clipped restoration raster to: {clipped_raster_path}") + + if push_to_geoserver: + push_local_raster_to_geoserver( + file_path=str(clipped_raster_path), + layer_name=layer_name, + workspace=GEOSERVER_WORKSPACE, + style_name=RESTORATION_STYLE_NAME, + ) + print(f"Pushed raster {layer_name} to GeoServer.") + + if sync_layer_metadata and state and district and block: + raster_layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=str(clipped_raster_path), + dataset_name="Restoration Raster", + misc={"is_generated_locally": True}, + ) + if raster_layer_id: + update_layer_sync_status(layer_id=raster_layer_id, sync_to_geoserver=True) + print(f"Database record updated for raster layer_id: {raster_layer_id}") + + # STAC Specs for Raster + # try: + # layer_STAC_generated = generate_STAC_layerwise.generate_raster_stac( + # state=state, + # district=district, + # block=block, + # layer_name="wri_restoration_raster", + # ) + # update_layer_sync_status( + # layer_id=raster_layer_id, is_stac_specs_generated=layer_STAC_generated + # ) + # print("STAC metadata updated for restoration raster") + # except Exception as e: + # print(f"Error generating STAC for raster: {e}") + + return True, clipped_raster_path + + +def run_vector_restoration_local( + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + raster_path=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if not raster_path: + raise ValueError("`raster_path` is required for vector stage.") + + if state and district and block: + layer_name = f"restoration_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_vector" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + else: + layer_name = f"{asset_suffix}_restoration_vector".lower() + watersheds_gdf = read_validated_vector_file(roi_path, f"Invalid ROI file: {roi_path}") + + # ========================================== + # 2. Vector Processing + # ========================================== + print("Computing restoration class areas per watershed...") + vector_result_gdf = compute_categorical_raster_areas_for_watersheds( + watersheds_gdf=watersheds_gdf, + raster_path=raster_path, + class_definitions=RESTORATION_CLASSES, + ) + + desired_cols = [ + "uid", + "id", + "area_in_ha", + "Excluded A", + "Mosaic Res", + "Protection", + "Wide-scale", + "geometry" + ] + keep_cols = [c for c in desired_cols if c in vector_result_gdf.columns] + vector_result_gdf = vector_result_gdf[keep_cols] + + output_vector_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_RESTORATION_OUTPUT, + ) + + vector_asset_id = write_vector_output( + gdf=vector_result_gdf, + output_path=output_vector_path, + layer_name=layer_name, + ) + print(f"Saved local restoration vector: {vector_asset_id}") + + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(vector_asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer response for vector: {geoserver_response}") + + if sync_layer_metadata and state and district and block: + vector_layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=vector_asset_id, + dataset_name="Restoration Vector", + misc={"is_generated_locally": True}, + ) + if vector_layer_id: + update_layer_sync_status(layer_id=vector_layer_id, sync_to_geoserver=True) + print(f"Database record updated for vector layer_id: {vector_layer_id}") + + # try: + # layer_STAC_generated = generate_STAC_layerwise.generate_vector_stac( + # state=state, + # district=district, + # block=block, + # layer_name="wri_restoration_vector", + # ) + # update_layer_sync_status( + # layer_id=vector_layer_id, + # is_stac_specs_generated=layer_STAC_generated, + # ) + # print("STAC metadata updated for restoration vector") + # except Exception as e: + # print(f"Error generating STAC for vector: {e}") + + return True + + +@app.task(bind=True) +def generate_restoration_opportunity_local( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + gee_account_id=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + + raster_ok, clipped_raster_path = run_raster_restoration_local( + state=state, + district=district, + block=block, + asset_suffix=asset_suffix, + roi_path=roi_path, + precomputed_roi_dir=precomputed_roi_dir, + push_to_geoserver=push_to_geoserver, + sync_layer_metadata=sync_layer_metadata, + ) + + if not raster_ok or not clipped_raster_path: + print("Raster stage failed — skipping vector stage.") + return False + + vector_ok = run_vector_restoration_local( + state=state, + district=district, + block=block, + asset_suffix=asset_suffix, + roi_path=roi_path, + raster_path=clipped_raster_path, + precomputed_roi_dir=precomputed_roi_dir, + push_to_geoserver=push_to_geoserver, + sync_layer_metadata=sync_layer_metadata, + ) + + return raster_ok and vector_ok diff --git a/computing/misc/river_local_compute.py b/computing/misc/river_local_compute.py new file mode 100644 index 00000000..6e5e83ec --- /dev/null +++ b/computing/misc/river_local_compute.py @@ -0,0 +1,293 @@ +import os +import json +import datetime +import pandas as pd +import geopandas as gpd +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.local_compute_helper import ( + PROJECT_ROOT, + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + build_output_vector_path, + get_watershed_areas_in_hectares, + load_precomputed_watersheds, + read_validated_vector_file, + validate_geometry, + write_vector_output, +) +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, + fix_invalid_geometry_in_gdf, +) + +from computing.config_loader import ( + PAN_INDIA_RIVER_PATH, + LOCAL_RIVER_OUTPUT, +) +from shapely.ops import unary_union + +GEOSERVER_WORKSPACE = "river" + +def _extract_lines(geom): + line_types = {"LineString", "MultiLineString", "LinearRing"} + if geom is None or geom.is_empty: + return None + + if geom.geom_type in line_types: + return geom + + # Polygon/MultiPolygon → take boundary + if geom.geom_type in {"Polygon", "MultiPolygon"}: + b = geom.boundary + return b if not b.is_empty else None + + # GeometryCollection → recurse and collect lines + if geom.geom_type == "GeometryCollection": + lines = [] + for part in geom.geoms: + extracted = _extract_lines(part) + if extracted and not extracted.is_empty: + lines.append(extracted) + if not lines: + return None + return unary_union(lines) + + return None + + +def _compute_river_properties_for_watersheds(watersheds_gdf, rivers_gdf): + + watersheds_gdf = validate_geometry(watersheds_gdf).reset_index(drop=True) + rivers_gdf = validate_geometry(rivers_gdf).reset_index(drop=True) + polygon_types = {"Polygon", "MultiPolygon"} + + if rivers_gdf.geometry.geom_type.isin(polygon_types).any(): + rivers_gdf = rivers_gdf.copy() + rivers_gdf["geometry"] = rivers_gdf.geometry.boundary + rivers_gdf = rivers_gdf[ + rivers_gdf.geometry.geom_type.isin( + ["LineString", "MultiLineString", "LinearRing"] + ) + ] + print(f"After boundary conversion: {len(rivers_gdf)} river features") + + outer_boundary = watersheds_gdf.geometry.unary_union + rivers_in_roi = rivers_gdf[rivers_gdf.intersects(outer_boundary)].copy() + + if rivers_in_roi.empty: + print("No rivers found within the outer boundary.") + return gpd.GeoDataFrame(columns=rivers_gdf.columns, crs=rivers_gdf.crs) + + watersheds_indexed = watersheds_gdf[["uid", "area_in_ha", "geometry"]].copy() + joined = gpd.sjoin( + rivers_in_roi, + watersheds_indexed, + how="inner", + predicate="intersects", + ) + + matched_river_indices = joined.index.unique() + gap_rivers = rivers_in_roi.loc[ + ~rivers_in_roi.index.isin(matched_river_indices) + ].copy() + + result_segments = [] + if not joined.empty: + clipped_rows = [] + for idx, row in joined.iterrows(): + try: + ws_idx = int(row["index_right"]) + ws_geom = watersheds_gdf.loc[ws_idx, "geometry"] + river_geom = row.geometry + + if not river_geom.is_valid: + river_geom = river_geom.buffer(0) + if not ws_geom.is_valid: + ws_geom = ws_geom.buffer(0) + + clipped = river_geom.intersection(ws_geom) + if clipped is None or clipped.is_empty: + continue + + clipped = _extract_lines(clipped) + if clipped is None or clipped.is_empty: + continue + + new_row = row.copy() + new_row["geometry"] = clipped + clipped_rows.append(new_row) + + except Exception as e: + print(f"Clip error river idx={idx}: {e}") + continue + + if clipped_rows: + matched_fc = gpd.GeoDataFrame(clipped_rows, crs=rivers_gdf.crs) + result_segments.append(matched_fc) + print(f"Valid matched segments: {len(clipped_rows)}") + + if not gap_rivers.empty: + clipped_gaps = [] + for idx, row in gap_rivers.iterrows(): + try: + clipped = row.geometry.intersection(outer_boundary) + if clipped is None or clipped.is_empty: + continue + + clipped = _extract_lines(clipped) + if clipped is None or clipped.is_empty: + continue + + new_row = row.copy() + new_row["geometry"] = clipped + new_row["uid"] = "" + new_row["area_in_ha"] = "" + clipped_gaps.append(new_row) + + except Exception as e: + print(f"Gap clip error river idx={idx}: {e}") + continue + + if clipped_gaps: + gap_fc = gpd.GeoDataFrame(clipped_gaps, crs=rivers_gdf.crs) + result_segments.append(gap_fc) + print(f"Valid gap segments: {len(clipped_gaps)}") + + if not result_segments: + print("No valid river segments after clipping.") + return gpd.GeoDataFrame(columns=rivers_gdf.columns, crs=rivers_gdf.crs) + + final_gdf = gpd.GeoDataFrame( + pd.concat(result_segments, ignore_index=True), + crs=rivers_gdf.crs, + ) + + final_gdf["uid"] = final_gdf["uid"].astype(str) + final_gdf["area_in_ha"] = final_gdf["area_in_ha"].astype(str) + + rename_cols = {} + if "st_area(sh" in final_gdf.columns: + rename_cols["st_area(sh"] = "st_area" + if "st_length(" in final_gdf.columns: + rename_cols["st_length("] = "st_length" + + if rename_cols: + final_gdf = final_gdf.rename(columns=rename_cols) + + for col in ["index_right"]: + if col in final_gdf.columns: + final_gdf = final_gdf.drop(columns=[col]) + + final_gdf = final_gdf[~final_gdf.geometry.is_empty] + final_gdf = final_gdf[final_gdf.geometry.is_valid] + final_gdf = final_gdf[final_gdf.geometry.notna()] + final_gdf = fix_invalid_geometry_in_gdf(final_gdf) + + final_gdf = final_gdf[ + final_gdf.geometry.apply( + lambda g: g is not None + and not g.is_empty + and g.bounds[0] <= g.bounds[2] + and g.bounds[1] <= g.bounds[3] + ) + ] + + print(f"Final valid river segments: {len(final_gdf)}") + return final_gdf + + +@app.task(bind=True) +def river_vector( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi=None, + asset_folder_list=None, + app_type="MWS", + gee_account_id=None, + river_vector_path=PAN_INDIA_RIVER_PATH, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + """ + Celery task for local river vector generation. + """ + if state and district and block: + layer_name = f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_river_vector" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + if not roi or not asset_suffix: + raise ValueError( + "For non state/district/block runs, both `roi` and `asset_suffix` are required." + ) + layer_name = f"{asset_suffix}_river_vector".lower() + watersheds_gdf = read_validated_vector_file( + roi, + f"ROI file has no valid geometries: {roi}", + ) + + if not os.path.exists(river_vector_path): + raise FileNotFoundError(f"River source file not found: {river_vector_path}") + + print(f"Loading river source: {river_vector_path}") + rivers_gdf = read_validated_vector_file( + river_vector_path, + f"River source file has no valid geometries: {river_vector_path}", + ) + + result_gdf = _compute_river_properties_for_watersheds( + watersheds_gdf=watersheds_gdf, + rivers_gdf=rivers_gdf, + ) + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_RIVER_OUTPUT, + ) + + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local river vector: {asset_id}") + + if push_to_geoserver: + push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="River Vector", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for river vector") + + return True + + diff --git a/computing/misc/slope_percentage_local_compute.py b/computing/misc/slope_percentage_local_compute.py new file mode 100644 index 00000000..fd29de2a --- /dev/null +++ b/computing/misc/slope_percentage_local_compute.py @@ -0,0 +1,117 @@ +import os +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.utils import ( + save_layer_info_to_db, + update_layer_sync_status, +) +from computing.local_compute_helper import ( + PROJECT_ROOT, + build_output_raster_path, + load_precomputed_watersheds, + read_validated_vector_file, + clip_raster_with_roi, + push_local_raster_to_geoserver, +) +from computing.STAC_specs import generate_STAC_layerwise +from computing.config_loader import ( + PAN_INDIA_SLOPE_PERCENTAGE_PATH, + LOCAL_SLOPE_PERCENTAGE_OUTPUT, +) + +GEOSERVER_WORKSPACE = "slope_percentage" +SLOPE_PERCENTAGE_STYLE_NAME = "slope_percentage" + + +@app.task(bind=True) +def generate_slope_percentage_data_local( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + gee_account_id=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if state and district and block: + layer_name_base = f"slope_percentage_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + if not roi_path or not asset_suffix: + raise ValueError("ROI path and asset_suffix are required for custom runs.") + layer_name_base = f"slope_percentage_{valid_gee_text(asset_suffix).lower()}" + watersheds_gdf = read_validated_vector_file( + roi_path, f"Invalid ROI file: {roi_path}" + ) + print(f"ROI source: {roi_path}") + + # Raster Processing + raster_layer_name = f"{layer_name_base}" + output_raster_path = build_output_raster_path( + layer_name=raster_layer_name, + output_base_dir=LOCAL_SLOPE_PERCENTAGE_OUTPUT, + state=state, + district=district, + block=block, + ) + + print("Clipping Slope Percentage raster...") + clipped_raster_path = clip_raster_with_roi( + roi_gdf=watersheds_gdf, + raster_path=PAN_INDIA_SLOPE_PERCENTAGE_PATH, + output_path=output_raster_path, + raster_label="Slope Percentage Raster", + ) + print(f"Saved clipped Slope Percentage raster to: {clipped_raster_path}") + + layer_at_geoserver = False + if push_to_geoserver: + push_local_raster_to_geoserver( + file_path=str(clipped_raster_path), + layer_name=raster_layer_name, + workspace=GEOSERVER_WORKSPACE, + style_name=SLOPE_PERCENTAGE_STYLE_NAME, + ) + print(f"Pushed raster {raster_layer_name} to GeoServer.") + layer_at_geoserver = True + + if sync_layer_metadata and state and district and block: + raster_layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=raster_layer_name, + asset_id=str(clipped_raster_path), + dataset_name="Slope Percentage", + misc={"is_generated_locally": True}, + ) + if raster_layer_id: + update_layer_sync_status(layer_id=raster_layer_id, sync_to_geoserver=True) + print(f"Database record updated for raster layer_id: {raster_layer_id}") + + # STAC Specs for Raster + try: + layer_STAC_generated = generate_STAC_layerwise.generate_raster_stac( + state=state, + district=district, + block=block, + layer_name="slope_percentage_raster", + ) + update_layer_sync_status( + layer_id=raster_layer_id, + is_stac_specs_generated=layer_STAC_generated, + ) + print("STAC metadata updated for Slope Percentage raster") + except Exception as e: + print(f"Error generating STAC for raster: {e}") + + return layer_at_geoserver diff --git a/computing/misc/soge_vector_local_compute.py b/computing/misc/soge_vector_local_compute.py new file mode 100644 index 00000000..b022d92f --- /dev/null +++ b/computing/misc/soge_vector_local_compute.py @@ -0,0 +1,218 @@ +import os +import geopandas as gpd +import pandas as pd + +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) +from computing.local_compute_helper import ( + PROJECT_ROOT, + build_output_vector_path, + load_precomputed_watersheds, + read_validated_vector_file, + write_vector_output, + validate_geometry, +) +from computing.STAC_specs import generate_STAC_layerwise + +from computing.config_loader import ( + PAN_INDIA_SOGE_PATH, + LOCAL_SOGE_OUTPUT, +) + +GEOSERVER_WORKSPACE = "soge" + + +def _compute_soge_for_watersheds(watersheds_gdf, soge_gdf): + """ + Finds the largest intersecting SOGE feature for each watershed and transfers its properties. + Mimics the Earth Engine implementation using GeoPandas overlay and area calculations. + """ + # Calculate watershed area in ha if missing + if "area_in_ha" not in watersheds_gdf.columns: + mws_metric = watersheds_gdf.to_crs("EPSG:6933") + watersheds_gdf["area_in_ha"] = mws_metric.geometry.area / 10000.0 + + if soge_gdf.empty: + # Create an empty dataframe to merge, forcing the No Data logic below + largest_intersections = pd.DataFrame(columns=["uid"]) + else: + # Reproject to metric CRS for accurate area intersection + metric_crs = "EPSG:6933" + mws_metric = watersheds_gdf.to_crs(metric_crs) + soge_metric = soge_gdf.to_crs(metric_crs) + + # Calculate intersections + intersection_gdf = gpd.overlay(mws_metric, soge_metric, how='intersection') + intersection_gdf["intersection_area_ha"] = intersection_gdf.geometry.area / 10000.0 + + # Sort and keep the largest intersection per watershed uid + intersection_gdf = intersection_gdf.sort_values(by="intersection_area_ha", ascending=False) + largest_intersections = intersection_gdf.drop_duplicates(subset=["uid"], keep="first").copy() + + # Calculate percentage area + largest_intersections["pct_area_soge"] = ( + largest_intersections["intersection_area_ha"] / largest_intersections["area_in_ha"] + ) * 100.0 + + # Rename columns to match desired output + col_mapping = { + "intersection_area_ha": "max_intersection_area_ha", + "block": "soge_block", + "district": "soge_district", + "objectid": "soge_objectid", + "state": "soge_state", + "tehsil": "soge_tehsil", + } + largest_intersections = largest_intersections.rename(columns=col_mapping) + + # Columns to transfer from SOGE to MWS + target_cols = [ + "uid", "max_intersection_area_ha", "pct_area_soge", "class", + "agwd_dom_i", "agwd_irr", "agwd_tot", "ar_gwr_tot", "code", + "gwr_2011_2", "na_gwa", "nat_discha", "sgw_dev_pe", + "soge_block", "soge_district", "soge_objectid", "soge_state", "soge_tehsil" + ] + + # Keep only target columns that actually exist in the intersections + cols_to_keep = [c for c in target_cols if c in largest_intersections.columns] + + # Left join to retain all original watersheds + result_gdf = watersheds_gdf.merge(largest_intersections[cols_to_keep], on="uid", how="left") + + # Fill defaults for No Data / non-intersecting + result_gdf["max_intersection_area_ha"] = result_gdf.get("max_intersection_area_ha", pd.Series(dtype=float)).fillna(0) + result_gdf["pct_area_soge"] = result_gdf.get("pct_area_soge", pd.Series(dtype=float)).fillna(0) + result_gdf["class"] = result_gdf.get("class", pd.Series(dtype=str)).fillna("No Data") + + numeric_cols = [ + "agwd_dom_i", "agwd_irr", "agwd_tot", "ar_gwr_tot", "code", + "gwr_2011_2", "na_gwa", "nat_discha", "sgw_dev_pe", "soge_objectid" + ] + for c in numeric_cols: + if c in result_gdf.columns: + result_gdf[c] = result_gdf[c].fillna(-9999) + else: + result_gdf[c] = -9999 + + string_cols = ["soge_block", "soge_district", "soge_state", "soge_tehsil"] + for c in string_cols: + if c in result_gdf.columns: + result_gdf[c] = result_gdf[c].fillna("") + else: + result_gdf[c] = "" + + return result_gdf + + +@app.task(bind=True) +def generate_soge_vector_local( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + gee_account_id=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if state and district and block: + layer_name = f"soge_vector_{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + if not roi_path or not asset_suffix: + raise ValueError("ROI path and asset_suffix are required for custom runs.") + layer_name = f"soge_vector_{asset_suffix}".lower() + watersheds_gdf = read_validated_vector_file(roi_path, f"Invalid ROI file: {roi_path}") + print(f"ROI source: {roi_path}") + + if not os.path.exists(PAN_INDIA_SOGE_PATH): + print(f"Warning: PAN INDIA SOGE file not found at {PAN_INDIA_SOGE_PATH}. Proceeding with No Data.") + # Create empty dummy GDF + soge_gdf = gpd.GeoDataFrame(geometry=[]) + else: + print("Loading SOGE data overlapping ROI...") + soge_gdf = gpd.read_file(PAN_INDIA_SOGE_PATH, mask=watersheds_gdf) + soge_gdf = validate_geometry(soge_gdf) + if soge_gdf.empty: + raise ValueError("PAN INDIA SOGE file has no valid geometries overlapping ROI") + print(f"Loaded {len(soge_gdf)} SOGE features") + + result_gdf = _compute_soge_for_watersheds( + watersheds_gdf=watersheds_gdf, + soge_gdf=soge_gdf, + ) + print(f"Final valid SOGE mapped features: {len(result_gdf)}") + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_SOGE_OUTPUT, + ) + + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local SOGE vector: {asset_id}") + + layer_at_geoserver = False + + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer response: {geoserver_response}") + if geoserver_response and geoserver_response.get("status_code") in (200, 201): + layer_at_geoserver = True + + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="SOGE", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for SOGE vector") + + try: + layer_STAC_generated = generate_STAC_layerwise.generate_vector_stac( + state=state, + district=district, + block=block, + layer_name="stage_of_groundwater_extraction_vector", + ) + update_layer_sync_status( + layer_id=layer_id, is_stac_specs_generated=layer_STAC_generated + ) + print("STAC metadata updated for SOGE vector") + except Exception as e: + print(f"Error generating STAC: {e}") + + return layer_at_geoserver + + + diff --git a/computing/mws/mws_centroid_local_compute.py b/computing/mws/mws_centroid_local_compute.py new file mode 100644 index 00000000..679dfdc0 --- /dev/null +++ b/computing/mws/mws_centroid_local_compute.py @@ -0,0 +1,125 @@ +import os +import geopandas as gpd + +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, +) +from computing.local_compute_helper import ( + PROJECT_ROOT, + build_output_vector_path, + load_precomputed_watersheds, + read_validated_vector_file, + write_vector_output, +) +from computing.config_loader import LOCAL_MWS_CENTROID_OUTPUT + +GEOSERVER_WORKSPACE = "mws_centroid" + + +def _compute_mws_centroids(watersheds_gdf): + """ + Computes the centroid of each watershed polygon and extracts lat/lon. + """ + if watersheds_gdf.empty: + return watersheds_gdf + + # Create a copy so we don't modify the original + centroids_gdf = watersheds_gdf.copy() + + # Ensure WGS84 for correct lat/lon coordinate extraction + if centroids_gdf.crs != "EPSG:4326": + centroids_gdf = centroids_gdf.to_crs("EPSG:4326") + + # Replace polygon geometry with point centroid + centroids_gdf["geometry"] = centroids_gdf.geometry.centroid + + # Extract coordinates + centroids_gdf["centroid_lon"] = centroids_gdf.geometry.x + centroids_gdf["centroid_lat"] = centroids_gdf.geometry.y + + return centroids_gdf + + +@app.task(bind=True) +def generate_mws_centroid_data_local( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + gee_account_id=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + _ = self, gee_account_id + if state and district and block: + layer_name = f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_mws_centroid" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + if not roi_path or not asset_suffix: + raise ValueError("ROI path and asset_suffix are required for custom runs.") + layer_name = f"{valid_gee_text(asset_suffix).lower()}_mws_centroid" + watersheds_gdf = read_validated_vector_file( + roi_path, f"Invalid ROI file: {roi_path}" + ) + print(f"ROI source: {roi_path}") + + print("Computing MWS centroids...") + result_gdf = _compute_mws_centroids(watersheds_gdf=watersheds_gdf) + print(f"Computed centroids for {len(result_gdf)} features") + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_MWS_CENTROID_OUTPUT, + ) + + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local MWS centroid vector: {asset_id}") + + layer_at_geoserver = False + + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer response: {geoserver_response}") + if geoserver_response and geoserver_response.get("status_code") in (200, 201): + layer_at_geoserver = True + + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Mws Centroid", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for MWS Centroid vector") + + return layer_at_geoserver diff --git a/computing/mws/mws_connectivity_local_compute.py b/computing/mws/mws_connectivity_local_compute.py new file mode 100644 index 00000000..f589b010 --- /dev/null +++ b/computing/mws/mws_connectivity_local_compute.py @@ -0,0 +1,139 @@ +import os +import json +import datetime +import pandas as pd +import geopandas as gpd +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text +from computing.local_compute_helper import ( + PROJECT_ROOT, + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + build_output_vector_path, + get_watershed_areas_in_hectares, + load_precomputed_watersheds, + read_validated_vector_file, + validate_geometry, + write_vector_output, +) +from computing.config_loader import ( + PAN_INDIA_MWS_CONNECTIVITY_PATH, + LOCAL_MWS_CONNECTIVITY_OUTPUT, +) +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + update_layer_sync_status, + fix_invalid_geometry_in_gdf, +) + +GEOSERVER_WORKSPACE = "mws_connectivity" + +from shapely.ops import unary_union + + +def _compute_mws_connectivity_for_watersheds(watersheds_gdf, mws_gdf): + if mws_gdf.empty: + print("No MWS connectivity found within the outer boundary.") + return mws_gdf + + # Step 1: Clip to the outer boundary of watersheds + outer_boundary = watersheds_gdf.geometry.unary_union + mws_in_roi = gpd.clip(mws_gdf, outer_boundary).copy() + + # Step 2: Drop empty/invalid geometries + mws_in_roi = fix_invalid_geometry_in_gdf(mws_in_roi) + mws_in_roi = mws_in_roi[ + mws_in_roi.geometry.notna() + & ~mws_in_roi.geometry.is_empty + & mws_in_roi.geometry.is_valid + ] + + print(f"Final valid MWS connectivity: {len(mws_in_roi)}") + return mws_in_roi + + +@app.task(bind=True) +def mws_connectivity_vector( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + if state and district and block: + layer_name = f"{valid_gee_text(district.lower())}_{valid_gee_text(block.lower())}_mws_connectivity" + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + print(f"Watershed boundary source: {watershed_source}") + else: + if not roi or not asset_suffix: + raise ValueError( + "For non state/district/block runs, both `roi` and `asset_suffix` are required." + ) + layer_name = f"{asset_suffix}_mws_connectivity".lower() + watersheds_gdf = read_validated_vector_file( + roi, + f"ROI file has no valid geometries: {roi}", + ) + print(f"ROI source: {roi}") + + if not os.path.exists(PAN_INDIA_MWS_CONNECTIVITY_PATH): + raise FileNotFoundError(f"PAN INDIA MWS connectivity file not found") + + mws_gdf = gpd.read_file(PAN_INDIA_MWS_CONNECTIVITY_PATH, mask=watersheds_gdf) + mws_gdf = validate_geometry(mws_gdf) + if mws_gdf.empty: + print("Warning: PAN INDIA MWS connectivity file has no valid geometries overlapping ROI") + + result_gdf = _compute_mws_connectivity_for_watersheds( + watersheds_gdf=watersheds_gdf, + mws_gdf=mws_gdf, + ) + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_MWS_CONNECTIVITY_OUTPUT, + ) + + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local MWS connectivity vector: {asset_id}") + + if push_to_geoserver: + geoserver_response = push_shape_to_geoserver( + os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + print(f"GeoServer response: {geoserver_response}") + + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Mws Connectivity", + misc={"is_generated_locally": True}, + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for MWS connectivity vector") + + return True diff --git a/computing/signals.py b/computing/signals.py index e103c579..a52bdb1f 100644 --- a/computing/signals.py +++ b/computing/signals.py @@ -1,13 +1,4 @@ -"""Auto-trigger STAC collection generation when a Layer is synced to GeoServer. - -The handler resolves the saved `Layer` (which stores a *templated* GeoServer -layer name) back to the canonical STAC `layer_name` / `layer_type` via the -`LayerMapping` registry (sourced from `layer_mapping.csv`), then dispatches the -`generate_stac_collection_task` Celery task asynchronously. - -This removes the need for every layer-generation task to hardcode the STAC -parameters and to call STAC generation synchronously. -""" +"""Auto-trigger STAC collection generation when a Layer is synced to GeoServer.""" from __future__ import annotations @@ -16,117 +7,68 @@ from django.db.models.signals import post_save from django.dispatch import receiver -from computing.models import Layer, LayerMapping -from utilities.gee_utils import valid_gee_text - -log = logging.getLogger(__name__) +from computing.models import Layer +from computing.stac_layer_resolution import stac_task_kwargs_for_layer +from utilities.layer_generation_mode import ( + is_sync_layer_generation_context_active, + record_sync_stac_layer, +) +# Same logger as layer task steps so Apache/WSGI error logs show STAC skips. +log = logging.getLogger("core_stack.layer_generation") _STAC_QUEUE = "nrm" -def _format_geoserver_name(template: str, layer: Layer) -> str: - if not template: - return "" - misc = layer.misc or {} - try: - return template.format( - district=valid_gee_text(layer.district.district_name.lower()), - block=valid_gee_text(layer.block.tehsil_name.lower()), - state=valid_gee_text(layer.state.state_name.lower()), - start_year=str(misc.get("start_year", "") or ""), - end_year=str(misc.get("end_year", "") or ""), - ) - except (KeyError, IndexError, AttributeError): - return "" - - -def _resolve_mapping(layer: Layer) -> LayerMapping | None: - """Return the best matching LayerMapping for a saved Layer. - - Resolution is keyed on `dataset.name -> LayerMapping.db_dataset_name`. - For datasets that fan out into multiple STAC layers (e.g. Change Detection - has 5 sub-layers per dataset row), we disambiguate by formatting each - candidate's `geoserver_layer_name` template against the Layer's location - and year metadata and matching the result against `layer.layer_name`. - """ - if not layer.dataset_id: - return None - - dataset_name = (layer.dataset.name or "").strip() - if not dataset_name: - return None - - candidates = list( - LayerMapping.objects.filter(db_dataset_name=dataset_name, auto_stac=True) - ) - if not candidates: - return None - if len(candidates) == 1: - return candidates[0] +@receiver(post_save, sender=Layer, dispatch_uid="computing.signals.trigger_stac_on_geoserver_sync") +def trigger_stac_on_geoserver_sync(sender, instance: Layer, created, **kwargs): + if not instance.is_sync_to_geoserver: + return - layer_name = (layer.layer_name or "").strip() - if not layer_name: - return None + if instance.is_stac_specs_generated: + log.info( + "STAC auto-trigger: skip layer id=%s (is_stac_specs_generated already True)", + instance.id, + ) + return - matches = [ - c - for c in candidates - if _format_geoserver_name(c.geoserver_layer_name, layer) == layer_name - ] - if len(matches) == 1: - return matches[0] - if not matches: + task_kwargs = stac_task_kwargs_for_layer(instance) + if task_kwargs is None: + dataset_name = instance.dataset.name if instance.dataset_id else None log.warning( - "STAC auto-trigger: no LayerMapping match for layer id=%s dataset=%s name=%s", - layer.id, + "STAC auto-trigger: skip layer id=%s name=%s dataset=%s " + "(no LayerMapping / CSV match; run: python manage.py load_layer_mappings)", + instance.id, + instance.layer_name, dataset_name, - layer_name, ) - return None - - log.info( - "STAC auto-trigger: %d ambiguous LayerMapping matches for layer id=%s; picking first", - len(matches), - layer.id, - ) - return matches[0] - - -@receiver(post_save, sender=Layer, dispatch_uid="computing.signals.trigger_stac_on_geoserver_sync") -def trigger_stac_on_geoserver_sync(sender, instance: Layer, created, **kwargs): - if not instance.is_sync_to_geoserver or instance.is_stac_specs_generated: return - mapping = _resolve_mapping(instance) - if mapping is None: - return - - # Lazy import: avoids importing Celery / stac_collection at app-loading time. from computing.STAC_specs.stac_collection import generate_stac_collection_task - misc = instance.misc or {} - task_kwargs = dict( - layer_type=mapping.layer_type, - state=instance.state.state_name, - district=instance.district.district_name, - block=instance.block.tehsil_name, - layer_name=mapping.layer_name, - start_year=str(misc.get("start_year", "") or ""), - end_year=str(misc.get("end_year", "") or ""), - upload_to_s3=True, - layer_id=instance.id, - ) - log.info( "STAC auto-trigger: dispatching task for layer id=%s (%s/%s)", instance.id, - mapping.layer_type, - mapping.layer_name, + task_kwargs["layer_type"], + task_kwargs["layer_name"], ) try: - generate_stac_collection_task.apply_async(kwargs=task_kwargs, queue=_STAC_QUEUE) + if is_sync_layer_generation_context_active(): + generate_stac_collection_task.apply(kwargs=task_kwargs) + record_sync_stac_layer( + state=task_kwargs["state"], + district=task_kwargs["district"], + block=task_kwargs["block"], + layer_name=task_kwargs["layer_name"], + layer_type=task_kwargs["layer_type"], + start_year=task_kwargs["start_year"], + end_year=task_kwargs["end_year"], + ) + else: + generate_stac_collection_task.apply_async( + kwargs=task_kwargs, queue=_STAC_QUEUE + ) except Exception as exc: # noqa: BLE001 log.error( "STAC auto-trigger: failed to dispatch task for layer id=%s: %s", diff --git a/computing/soil_health/soil_health.py b/computing/soil_health/soil_health.py new file mode 100644 index 00000000..4c92858c --- /dev/null +++ b/computing/soil_health/soil_health.py @@ -0,0 +1,497 @@ +import os +from pathlib import Path +import logging + +import numpy as np +import rasterio +from rasterio.mask import mask +from rasterio.warp import Resampling, reproject +from shapely.geometry import mapping + +from computing.config_loader import LULC_BASE_DIR +from computing.soil_health.soil_health_helper import ( + nutrient_stats_for_geometries, + lulc_area_stats_for_geometries, +) +from computing.utils import save_layer_info_to_db, update_layer_sync_status +from utilities.gee_utils import valid_gee_text +from computing.local_compute_helper import ( + PROJECT_ROOT, + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + build_output_vector_path, + build_output_raster_path, + compute_mode_lulc_array, + read_validated_vector_file, + validate_geometry, + write_vector_output, + push_local_vector_to_geoserver, + push_local_raster_to_geoserver, + load_precomputed_roi, +) + +from nrm_app.celery import app + +logger = logging.getLogger(__name__) + +LOCAL_OUTPUT_BASE_DIR = "data/soil_health" +GEOSERVER_STYLE = "" +GEOSERVER_RASTER_WORKSPACE = "soil_health_raster" +GEOSERVER_VECTOR_WORKSPACE = "soil_health_vector" +NUTRIENTS = ["N", "K", "P", "OC", "OC_OLM"] +NUTRIENT_PERCENTILES = (5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95) +LOCAL_ALGORITHM = "local_soil_health" +LOCAL_ALGORITHM_VERSION = "local-1.0" + + +def _get_lulc_mask_classes(nutrient): + nutrient = str(nutrient).strip().upper() + if nutrient == "OC_OLM": + return {6, 12} + if nutrient in { + "N", + "P", + "K", + "OC", + }: + return {8, 9, 10, 11} + raise ValueError(f"Unsupported nutrient for LULC masking: {nutrient}") + + +def _pick_output_nodata(dtype, source_nodata): + if source_nodata is not None: + source_nodata = float(source_nodata) + if not np.isnan(source_nodata): + return source_nodata + + dtype = np.dtype(dtype) + if np.issubdtype(dtype, np.floating): + return -9999.0 + + info = np.iinfo(dtype) + if np.issubdtype(dtype, np.signedinteger): + return info.min + return info.max + + +def _resolve_latest_lulc_raster_paths(count=3, lulc_dir=LULC_BASE_DIR): + lulc_dir = Path(lulc_dir) + if not lulc_dir.exists(): + return [] + + candidates = sorted(lulc_dir.glob("lulc_v3_*.tif")) + if not candidates: + return [] + selected = candidates[-count:] if len(candidates) >= count else candidates + return [str(path) for path in selected] + + +def _clip_raster_to_roi( + roi_gdf, + raster_path, +): + with rasterio.open(raster_path) as soil_src: + roi_gdf = validate_geometry(roi_gdf) + if roi_gdf.empty: + raise ValueError( + "No valid ROI geometry available for soil health clipping." + ) + if roi_gdf.crs is None: + raise ValueError( + "ROI CRS is missing; cannot align with soil health raster." + ) + if soil_src.crs and roi_gdf.crs != soil_src.crs: + roi_gdf = roi_gdf.to_crs(soil_src.crs) + + shapes = [ + mapping(geom) + for geom in roi_gdf.geometry + if geom is not None and not geom.is_empty + ] + if not shapes: + raise ValueError( + "No valid ROI geometry available for soil health clipping." + ) + + band_index = 1 + nodata = _pick_output_nodata( + dtype=soil_src.dtypes[band_index - 1], + source_nodata=soil_src.nodata, + ) + clipped_data, clipped_transform = mask( + soil_src, + shapes=shapes, + crop=True, + filled=True, + nodata=nodata, + indexes=band_index, + ) + if clipped_data.ndim == 3: + clipped_data = clipped_data[0] + + clipped_meta = soil_src.meta.copy() + clipped_meta.update( + { + "driver": "GTiff", + "height": clipped_data.shape[0], + "width": clipped_data.shape[1], + "transform": clipped_transform, + "count": 1, + "dtype": clipped_data.dtype, + "nodata": nodata, + "compress": "lzw", + } + ) + + return clipped_data, clipped_meta + + +def _prepare_mode_lulc( + reference_meta, + lulc_paths, +): + """ + Reprojects the latest LULC rasters to the reference grid and + returns the 3-year mode LULC. + """ + + reprojected_arrays = [] + + for lulc_path in lulc_paths: + + lulc_array = np.zeros( + ( + reference_meta["height"], + reference_meta["width"], + ), + dtype=np.float32, + ) + + with rasterio.open(lulc_path) as src: + + reproject( + source=rasterio.band(src, 1), + destination=lulc_array, + src_transform=src.transform, + src_crs=src.crs, + src_nodata=src.nodata, + dst_transform=reference_meta["transform"], + dst_crs=reference_meta["crs"], + dst_nodata=0, + resampling=Resampling.mode, + ) + + reprojected_arrays.append(lulc_array) + + return compute_mode_lulc_array(reprojected_arrays) + + +def _apply_lulc_mask_and_write( + clipped_data, + clipped_meta, + output_path, + lulc_mode_array=None, + allowed_mask_classes=None, +): + nodata = clipped_meta["nodata"] + + if clipped_data.shape != lulc_mode_array.shape: + raise ValueError( + f"LULC shape {lulc_mode_array.shape} " + f"does not match soil raster {clipped_data.shape}" + ) + + valid_pixels = np.isin( + lulc_mode_array, + list(allowed_mask_classes), + ) + + valid_soil_pixels = clipped_data != nodata + + output_array = np.where( + valid_pixels & valid_soil_pixels, + clipped_data, + nodata, + ) + + with rasterio.open(output_path, "w", **clipped_meta) as dst: + dst.write(output_array.astype(clipped_meta["dtype"]), 1) + + return str(output_path) + + +def clip_soil_health_raster( + state=None, + district=None, + block=None, + asset_suffix=None, + roi=None, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + push_to_geoserver=True, + sync_layer_metadata=False, +): + + asset_suffix, roi_gdf = get_roi( + asset_suffix, + block, + district, + roi, + state, + precomputed_roi_dir=precomputed_roi_dir, + ) + layer_name = f"{asset_suffix}_soil_health_raster" + + lulc_paths = _resolve_latest_lulc_raster_paths() + + if not lulc_paths: + raise ValueError("No LULC rasters found to prepare 3-year mode.") + + reference_data, reference_meta = _clip_raster_to_roi( + roi_gdf=roi_gdf, + raster_path=str( + PROJECT_ROOT / "data/base_layers/soil_health/soil_health_N.tif" + ), + ) + + lulc_mode_array = _prepare_mode_lulc( + reference_meta=reference_meta, + lulc_paths=lulc_paths, + ) + + if lulc_mode_array.shape != reference_data.shape: + raise ValueError("Prepared LULC mode does not match reference raster grid.") + + geoserver_statuses = [] + for nutrient in NUTRIENTS: + SOIL_MAP_PATH = str( + PROJECT_ROOT / f"data/base_layers/soil_health/soil_health_{nutrient}.tif" + ) + output_raster_path = build_output_raster_path( + layer_name=f"{layer_name}_{nutrient}", + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + state=state, + district=district, + block=block, + ) + + if nutrient == "N": + clipped_data = reference_data + clipped_meta = reference_meta + else: + clipped_data, clipped_meta = _clip_raster_to_roi( + roi_gdf, + SOIL_MAP_PATH, + ) + + allowed_mask_classes = _get_lulc_mask_classes(nutrient) + asset_id = _apply_lulc_mask_and_write( + clipped_data=clipped_data, + clipped_meta=clipped_meta, + output_path=output_raster_path, + lulc_mode_array=lulc_mode_array, + allowed_mask_classes=allowed_mask_classes, + ) + + if push_to_geoserver: + upload_res, style_res = push_local_raster_to_geoserver( + file_path=output_raster_path, + layer_name=f"{layer_name}_{nutrient}", + workspace=GEOSERVER_RASTER_WORKSPACE, + ) + print(f"GeoServer upload response for {nutrient}: {upload_res}") + geoserver_statuses.append(True) + + if sync_layer_metadata: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Soil Health Raster", + misc={"is_generated_locally": True}, + algorithm=LOCAL_ALGORITHM, + algorithm_version=LOCAL_ALGORITHM_VERSION, + ) + logger.info("Saved layer metadata to DB: layer_id=%s", layer_id) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for Soil health raster") + + return ( + all(geoserver_statuses) if push_to_geoserver else True, + lulc_mode_array, + reference_meta, + ) + + +def get_roi(asset_suffix, block, district, roi, state, precomputed_roi_dir): + if state and district and block: + asset_suffix = f"{valid_gee_text(str(district).lower())}_{valid_gee_text(str(block).lower())}" + roi_gdf = load_precomputed_roi( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + else: + if not roi or not asset_suffix: + raise ValueError( + "For non state/district/block runs, both `roi` and `asset_suffix` are required." + ) + + roi_gdf = read_validated_vector_file( + roi, + f"ROI file has no valid geometries: {roi}", + ) + roi_gdf = validate_geometry(roi_gdf) + return asset_suffix, roi_gdf + + +def vectorize_soil_health( + state=None, + district=None, + block=None, + asset_suffix=None, + roi=None, + push_to_geoserver=True, + sync_layer_metadata=True, + lulc_mode=None, + lulc_meta=None, +): + + asset_suffix, roi_gdf = get_roi( + asset_suffix, + block, + district, + roi, + state, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + ) + base_layer_name = f"{asset_suffix}_soil_health" + output_layer_name = f"{base_layer_name}_vector" + result_gdf = roi_gdf.copy() + + for nutrient in NUTRIENTS: + raster_path = build_output_raster_path( + layer_name=f"{base_layer_name}_raster_{nutrient}", + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + state=state, + district=district, + block=block, + ) + + nutrient_gdf = nutrient_stats_for_geometries( + roi_gdf=roi_gdf, + raster_path=raster_path, + percentiles=tuple(NUTRIENT_PERCENTILES), + nutrient=nutrient, + ) + nutrient_columns = [ + column + for column in nutrient_gdf.columns + if column.startswith(f"{nutrient}_") + ] + for column in nutrient_columns: + result_gdf[column] = nutrient_gdf[column] + + lulc_area_gdf = lulc_area_stats_for_geometries( + roi_gdf=result_gdf, + lulc_mode=lulc_mode, + lulc_meta=lulc_meta, + ) + + result_gdf["crop_cover_area"] = lulc_area_gdf["crop_cover_area"] + + result_gdf["tree_shrub_area"] = lulc_area_gdf["tree_shrub_area"] + + output_path = build_output_vector_path( + layer_name=output_layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + ) + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=output_layer_name, + ) + print(f"Saved soil health vector: {output_path}") + + geoserver_status = False + if push_to_geoserver: + geoserver_response = push_local_vector_to_geoserver( + path=os.path.splitext(output_path)[0], + layer_name=output_layer_name, + workspace=GEOSERVER_VECTOR_WORKSPACE, + file_type="gpkg", + ) + print(f"GeoServer response: {geoserver_response}") + + if not isinstance(geoserver_response, dict) or geoserver_response.get( + "status_code" + ) not in (200, 201): + return False + geoserver_status = True + + if sync_layer_metadata: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=output_layer_name, + asset_id=asset_id, + dataset_name="Soil Health Vector", + misc={"is_generated_locally": True}, + algorithm=LOCAL_ALGORITHM, + algorithm_version=LOCAL_ALGORITHM_VERSION, + ) + logger.info("Saved layer metadata to DB: layer_id=%s", layer_id) + if layer_id and push_to_geoserver: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Sync to GeoServer flag updated for Soil health vector") + + return geoserver_status if push_to_geoserver else True + + +@app.task(bind=True) +def soil_health_local( + self, + state=None, + district=None, + block=None, + asset_suffix=None, + roi=None, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + push_to_geoserver=True, + sync_layer_metadata=True, +): + soil_health_raster_on_geoserver, cached_lulc_mode, cached_lulc_meta = ( + clip_soil_health_raster( + state, + district, + block, + asset_suffix, + roi, + precomputed_roi_dir, + push_to_geoserver, + sync_layer_metadata, + ) + ) + + soil_health_vector_on_geoserver = vectorize_soil_health( + state, + district, + block, + asset_suffix, + roi, + push_to_geoserver, + sync_layer_metadata, + lulc_mode=cached_lulc_mode, + lulc_meta=cached_lulc_meta, + ) + return ( + True + if soil_health_vector_on_geoserver and soil_health_raster_on_geoserver + else False + ) diff --git a/computing/soil_health/soil_health_helper.py b/computing/soil_health/soil_health_helper.py new file mode 100644 index 00000000..6cfbd321 --- /dev/null +++ b/computing/soil_health/soil_health_helper.py @@ -0,0 +1,182 @@ +import rasterio +from rasterio.mask import mask +from shapely.geometry import mapping + + +from computing.local_compute_helper import ensure_file_exists + +from pyproj import Geod +import numpy as np + + +def _compute_row_area(transform, height): + """ + Compute the geodesic area (m²) of one pixel for every raster row. + + Since the raster is north-up in EPSG:4326, every pixel in the same + row has identical area. Only latitude changes between rows. + + Returns + ------- + np.ndarray + Shape = (height,) + Area (m²) of one pixel for each row. + """ + + geod = Geod(ellps="WGS84") + + row_area = np.zeros(height, dtype=np.float64) + + pixel_width = transform.a # degrees + pixel_height = abs(transform.e) # degrees + + west = transform.c + east = west + pixel_width + + for row in range(height): + + north = transform.f + row * transform.e + south = north + transform.e + + area, _ = geod.polygon_area_perimeter( + [west, east, east, west], + [north, north, south, south], + ) + + row_area[row] = abs(area) + + return row_area + + +def nutrient_stats_for_geometries(roi_gdf, raster_path, percentiles, nutrient): + ensure_file_exists(raster_path, "Clipped soil health raster") + + with rasterio.open(raster_path) as src: + # Raster masking expects geometries in the raster CRS, so reproject a + # working copy and preserve the original ROI CRS for the output vector. + working_gdf = roi_gdf.copy() + if working_gdf.crs is None: + raise ValueError( + "ROI CRS is missing; cannot align with soil health raster." + ) + if src.crs and working_gdf.crs != src.crs: + working_gdf = working_gdf.to_crs(src.crs) + + nodata = src.nodata + rows = [] + total = len(working_gdf) + for index, row in enumerate(working_gdf.itertuples(index=False), start=1): + geom = row.geometry + if geom is None or geom.is_empty: + values = np.array([], dtype=np.float64) + else: + # Clip the raster to one ROI/watershed polygon. `filled=False` + # keeps rasterio's mask, which lets us drop pixels outside the geometry. + try: + clipped, _ = mask( + src, + [mapping(geom)], + crop=True, + filled=False, + ) + except ValueError: + clipped = None + + if clipped is None or clipped.size == 0: + values = np.array([], dtype=np.float64) + else: + data = clipped[0] + valid_mask = ~np.ma.getmaskarray(data) + values = np.asarray(data, dtype=np.float64) + + # Ignore pixels outside the polygon, nodata pixels, and any + # non-finite values before calculating nutrient statistics. + valid_mask &= np.isfinite(values) + if nodata is not None and not np.isnan(nodata): + valid_mask &= values != float(nodata) + values = values[valid_mask] + + stats = {f"{nutrient}_count": int(values.size)} + if values.size == 0: + # Use NULLs in the vector table when a polygon has no valid raster pixels. + stats[f"{nutrient}_mean"] = None + for percentile in percentiles: + stats[f"{nutrient}_p{percentile:02d}"] = None + else: + stats[f"{nutrient}_mean"] = float(np.mean(values)) + percentile_values = np.percentile(values, percentiles) + for percentile, value in zip(percentiles, percentile_values): + stats[f"{nutrient}_p{percentile:02d}"] = float(value) + rows.append(stats) + + if index % 200 == 0 or index == total: + print(f"Computed soil health stats for {index}/{total} geometries") + + result = roi_gdf.copy() + # Attach the computed columns back to the original ROI geometries for output. + for column in rows[0] if rows else []: + result[column] = [row[column] for row in rows] + return result + + +def lulc_area_stats_for_geometries( + roi_gdf, + lulc_mode, + lulc_meta, +): + rows = [] + row_area = _compute_row_area( + lulc_meta["transform"], + lulc_mode.shape[0], + ) + transform = lulc_meta["transform"] + + for geom in roi_gdf.geometry: + + mask_arr = rasterio.features.geometry_mask( + [mapping(geom)], + out_shape=lulc_mode.shape, + transform=transform, + invert=True, + ) + + # crop_pixels = np.count_nonzero(mask_arr & np.isin(lulc_mode, [8, 9, 10, 11])) + + # tree_pixels = np.count_nonzero(mask_arr & np.isin(lulc_mode, [6, 12])) + + crop_mask = mask_arr & np.isin(lulc_mode, [8, 9, 10, 11]) + + tree_mask = mask_arr & np.isin(lulc_mode, [6, 12]) + + crop_area = 0.0 + tree_area = 0.0 + + height = lulc_mode.shape[0] + + for row in range(height): + + crop_pixels = np.count_nonzero(crop_mask[row]) + + tree_pixels = np.count_nonzero(tree_mask[row]) + + crop_area += crop_pixels * row_area[row] + + tree_area += tree_pixels * row_area[row] + + crop_area /= 10000.0 + tree_area /= 10000.0 + + rows.append( + { + "crop_cover_area": crop_area, + "tree_shrub_area": tree_area, + } + ) + + result = roi_gdf.copy() + + result["crop_cover_area"] = [r["crop_cover_area"] for r in rows] + + result["tree_shrub_area"] = [r["tree_shrub_area"] for r in rows] + + return result diff --git a/computing/soil_type/__init__.py b/computing/soil_type/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/computing/soil_type/__init__.py @@ -0,0 +1 @@ + diff --git a/computing/soil_type/soil_type_local.py b/computing/soil_type/soil_type_local.py new file mode 100644 index 00000000..813ebd45 --- /dev/null +++ b/computing/soil_type/soil_type_local.py @@ -0,0 +1,290 @@ +import logging +import os +from pathlib import Path + +import numpy as np +import pandas as pd +import rasterio +from rasterio.mask import mask +from shapely.geometry import mapping + +from computing.config_loader import ( + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + SOIL_TYPE_OUTPUT_DIR, + SOIL_TYPE_RASTER_PATHS, +) +from computing.local_compute_helper import ( + build_output_vector_path, + ensure_file_exists, + load_precomputed_watersheds, + push_local_vector_to_geoserver, + read_validated_vector_file, + write_vector_output, +) +from computing.utils import save_layer_info_to_db, update_layer_sync_status +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text + +logger = logging.getLogger(__name__) + +GEOSERVER_WORKSPACE = "soil_type" +LOCAL_ALGORITHM = "local_soil_type_zonal_summary" +LOCAL_ALGORITHM_VERSION = "local-1.0" +DATASET_NAME = "Soil Type" + +# TODO: water capacity could be a mean instead of dominant +AVAILABLE_WATER_CAPACITY_CLASSES = { + 1: 150, + 2: 125, + 3: 100, + 4: 75, + 5: 50, + 6: 15, + 7: 0, +} +SOIL_DRAINAGE_CLASSES = { + 0: "Excessively drained", + 1: "Somewhat excessively drained", + 2: "Well drained", + 3: "Moderately well drained", + 4: "Imperfectly drained", + 5: "Poorly drained", + 6: "Very poorly drained", +} +TOPSOIL_TEXTURE_CLASSES = { + 1: "Coarse", + 2: "Medium", + 3: "Fine", +} +SUBSOIL_TEXTURE_CLASSES = { + 1: "Clay (heavy)", + 2: "Silty clay", + 3: "Clay", + 4: "Silty clay loam", + 5: "Clay loam", + 6: "Silt", + 7: "Silt loam", + 8: "Sandy clay", + 9: "Loam", + 10: "Sandy clay loam", + 11: "Sandy loam", + 12: "Loamy sand", + 13: "Sand", +} + +SOIL_PROPERTY_SPECS = ( + { + "column": "available_water_capacity", + "aggregation": "mode", + "mapping": AVAILABLE_WATER_CAPACITY_CLASSES, + "zero_is_nodata": True, + }, + { + "column": "soil_drainage_classes", + "aggregation": "mode", + "mapping": SOIL_DRAINAGE_CLASSES, + "zero_is_nodata": False, + }, + {"column": "subsoil_bulk_density", "aggregation": "mean"}, + {"column": "subsoil_exchange_capacity", "aggregation": "mean"}, + {"column": "subsoil_organic_carbon", "aggregation": "mean"}, + {"column": "subsoil_ph", "aggregation": "mean"}, + { + "column": "subsoil_texture", + "aggregation": "mode", + "mapping": SUBSOIL_TEXTURE_CLASSES, + "zero_is_nodata": True, + }, + {"column": "topsoil_bulk_density", "aggregation": "mean"}, + {"column": "topsoil_exchange_capacity", "aggregation": "mean"}, + {"column": "topsoil_organic_carbon", "aggregation": "mean"}, + {"column": "topsoil_ph", "aggregation": "mean"}, + { + "column": "topsoil_texture", + "aggregation": "mode", + "mapping": TOPSOIL_TEXTURE_CLASSES, + "zero_is_nodata": True, + }, +) + + +def _slug(value, fallback): + return valid_gee_text(str(value).strip().lower()) or fallback + + +def _layer_name(district=None, block=None, asset_suffix=None): + if asset_suffix: + return f"{_slug(asset_suffix, 'custom')}_soil_type" + return ( + f"soil_type_{_slug(district, 'unknown_district')}_" + f"{_slug(block, 'unknown_block')}" + ) + + +def _aggregate_values(values, spec): + values = np.asarray(values, dtype=np.float64) + values = values[np.isfinite(values)] + if spec.get("zero_is_nodata", True): + values = values[values != 0] + if values.size == 0: + return None + + if spec["aggregation"] == "mean": + return round(float(values.mean()), 4) + + class_values = np.rint(values).astype(np.int32) + unique_values, counts = np.unique(class_values, return_counts=True) + mode = int(unique_values[np.argmax(counts)]) + return spec["mapping"].get(mode) + + +def compute_soil_properties_for_geometries( + geometries_gdf, + raster_paths=SOIL_TYPE_RASTER_PATHS, +): + if geometries_gdf.crs is None: + raise ValueError("Input geometry CRS is missing.") + + result = geometries_gdf.copy() + for spec in SOIL_PROPERTY_SPECS: + column = spec["column"] + raster_path = Path(raster_paths[column]) + ensure_file_exists(raster_path, f"Soil property raster '{column}'") + + with rasterio.open(raster_path) as src: + working_gdf = ( + geometries_gdf + if not src.crs or geometries_gdf.crs == src.crs + else geometries_gdf.to_crs(src.crs) + ) + values = [] + for geom in working_gdf.geometry: + if geom is None or geom.is_empty: + values.append(None) + continue + try: + clipped, _ = mask( + src, + [mapping(geom)], + crop=True, + filled=False, + ) + except ValueError: + values.append(None) + continue + + band = clipped[0] + valid = ~np.ma.getmaskarray(band) + data = np.asarray(band, dtype=np.float64) + if src.nodata is not None and np.isfinite(src.nodata): + valid &= data != src.nodata + values.append(_aggregate_values(data[valid], spec)) + + result[column] = pd.Series(values, index=result.index) + logger.info("Computed soil property column '%s'.", column) + + return result + + +def run_soil_type_local( + state=None, + district=None, + block=None, + asset_suffix=None, + roi_path=None, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + raster_paths=SOIL_TYPE_RASTER_PATHS, + output_base_dir=SOIL_TYPE_OUTPUT_DIR, + push_to_geoserver=True, + sync_layer_metadata=True, +): + is_tehsil_run = bool(state and district and block) + if is_tehsil_run: + state = str(state).strip().lower() + district = str(district).strip().lower() + block = str(block).strip().lower() + geometries_gdf, geometry_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + else: + if not roi_path or not asset_suffix: + raise ValueError( + "Custom runs require both `roi_path` and `asset_suffix`." + ) + geometries_gdf = read_validated_vector_file( + roi_path, + f"Custom ROI file has no valid geometries: {roi_path}", + ) + geometry_source = str(roi_path) + + logger.info("Soil type geometry source: %s", geometry_source) + layer_name = _layer_name(district, block, asset_suffix) + result_gdf = compute_soil_properties_for_geometries( + geometries_gdf=geometries_gdf, + raster_paths=raster_paths, + ) + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=output_base_dir, + ) + asset_id = write_vector_output(result_gdf, output_path, layer_name) + logger.info("Saved local soil type vector: %s", asset_id) + + geoserver_ok = False + geoserver_response = None + if push_to_geoserver: + geoserver_response = push_local_vector_to_geoserver( + path=os.path.splitext(asset_id)[0], + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + geoserver_ok = ( + isinstance(geoserver_response, dict) + and geoserver_response.get("status_code") in (200, 201) + ) + if not geoserver_ok: + logger.error( + "GeoServer upload failed for %s: %s", + layer_name, + geoserver_response, + ) + return False + + if sync_layer_metadata and is_tehsil_run: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name=DATASET_NAME, + misc={ + "is_generated_locally": True, + }, + algorithm=LOCAL_ALGORITHM, + algorithm_version=LOCAL_ALGORITHM_VERSION, + ) + if layer_id and push_to_geoserver: + update_layer_sync_status( + layer_id=layer_id, + sync_to_geoserver=True, + ) + + return geoserver_ok if push_to_geoserver else True + + +@app.task(bind=True) +def generate_soil_type_local(self, state=None, district=None, block=None): + _ = self + return run_soil_type_local( + state=state, + district=district, + block=block, + ) diff --git a/computing/soil_type/tests.py b/computing/soil_type/tests.py new file mode 100644 index 00000000..56351cb3 --- /dev/null +++ b/computing/soil_type/tests.py @@ -0,0 +1,177 @@ +from unittest import TestCase +from unittest.mock import MagicMock, patch + +import numpy as np +from django.urls import reverse +from rest_framework.test import APISimpleTestCase + +from computing.soil_type.soil_type_local import ( + _aggregate_values, + run_soil_type_local, +) + + +class SoilTypeAggregationTests(TestCase): + def test_mean_ignores_nan_and_zero_nodata(self): + spec = {"aggregation": "mean"} + self.assertEqual(_aggregate_values([0, 1.0, 2.0, np.nan], spec), 1.5) + + def test_mode_is_decoded(self): + spec = { + "aggregation": "mode", + "mapping": {1: "Coarse", 2: "Medium"}, + "zero_is_nodata": True, + } + self.assertEqual(_aggregate_values([0, 1, 2, 2, np.nan], spec), "Medium") + + def test_drainage_zero_is_a_valid_class(self): + spec = { + "aggregation": "mode", + "mapping": {0: "Excessively drained", 1: "Well drained"}, + "zero_is_nodata": False, + } + self.assertEqual( + _aggregate_values([0, 0, 1, np.nan], spec), + "Excessively drained", + ) + + +class SoilTypePublicationTests(TestCase): + def _run_with_mocks(self, geoserver_status): + geometries = MagicMock() + result = MagicMock() + patchers = ( + patch( + "computing.soil_type.soil_type_local.load_precomputed_watersheds", + return_value=(geometries, "/tmp/watersheds.gpkg"), + ), + patch( + "computing.soil_type.soil_type_local." + "compute_soil_properties_for_geometries", + return_value=result, + ), + patch( + "computing.soil_type.soil_type_local.build_output_vector_path", + return_value="/tmp/soil_type_test.gpkg", + ), + patch( + "computing.soil_type.soil_type_local.write_vector_output", + return_value="/tmp/soil_type_test.gpkg", + ), + patch( + "computing.soil_type.soil_type_local.push_local_vector_to_geoserver", + return_value={"status_code": geoserver_status}, + ), + patch( + "computing.soil_type.soil_type_local.save_layer_info_to_db", + return_value=42, + ), + patch( + "computing.soil_type.soil_type_local.update_layer_sync_status" + ), + ) + mocks = [patcher.start() for patcher in patchers] + self.addCleanup(lambda: [patcher.stop() for patcher in reversed(patchers)]) + + success = run_soil_type_local( + state="Puducherry", + district="Puducherry", + block="Bahur", + ) + return success, mocks + + def test_success_creates_workspace_and_syncs_cloud_metadata(self): + success, mocks = self._run_with_mocks(201) + geoserver_mock = mocks[4] + save_layer_mock = mocks[5] + update_sync_mock = mocks[6] + + self.assertTrue(success) + geoserver_mock.assert_called_once_with( + path="/tmp/soil_type_test", + workspace="soil_type", + layer_name="soil_type_puducherry_bahur", + file_type="gpkg", + ) + self.assertEqual( + save_layer_mock.call_args.kwargs["dataset_name"], + "Soil Type", + ) + self.assertEqual( + save_layer_mock.call_args.kwargs["layer_name"], + "soil_type_puducherry_bahur", + ) + update_sync_mock.assert_called_once_with( + layer_id=42, + sync_to_geoserver=True, + ) + + def test_failed_publication_does_not_update_cloud_metadata(self): + success, mocks = self._run_with_mocks(500) + + self.assertFalse(success) + mocks[5].assert_not_called() + mocks[6].assert_not_called() + + +class SoilTypeAPITests(APISimpleTestCase): + def setUp(self): + self.client.force_authenticate(user=MagicMock(is_authenticated=True)) + + @patch("computing.api.generate_soil_type_local.apply_async") + def test_generate_soil_type_queues_local_task(self, apply_async): + response = self.client.post( + reverse("generate_soil_type"), + { + "state": "Puducherry", + "district": "Puducherry", + "block": "Bahur", + "compute": "local", + }, + format="json", + ) + + self.assertEqual(response.status_code, 200) + apply_async.assert_called_once_with( + kwargs={ + "state": "puducherry", + "district": "puducherry", + "block": "bahur", + }, + queue="nrm", + ) + + @patch("computing.api.generate_soil_type_local.apply_async") + def test_generate_soil_type_rejects_missing_location(self, apply_async): + response = self.client.post( + reverse("generate_soil_type"), + {"state": "Puducherry", "compute": "local"}, + format="json", + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.json(), + {"Exception": "Missing required fields: district, block"}, + ) + apply_async.assert_not_called() + + @patch("computing.api.generate_soil_type_local.apply_async") + def test_generate_soil_type_rejects_non_local_compute(self, apply_async): + response = self.client.post( + reverse("generate_soil_type"), + { + "state": "Puducherry", + "district": "Puducherry", + "block": "Bahur", + "compute": "gee", + }, + format="json", + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.json(), + {"Exception": "Soil type only supports compute=local"}, + ) + apply_async.assert_not_called() diff --git a/computing/spei/__init__.py b/computing/spei/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/computing/spei/drought_sensitivity/__init__.py b/computing/spei/drought_sensitivity/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/computing/spei/drought_sensitivity/drought_resistance_resilience.py b/computing/spei/drought_sensitivity/drought_resistance_resilience.py new file mode 100644 index 00000000..29849d47 --- /dev/null +++ b/computing/spei/drought_sensitivity/drought_resistance_resilience.py @@ -0,0 +1,263 @@ +import ee + +from utilities.constants import AEZ +from utilities.gee_utils import ( + ee_initialize, + is_gee_asset_exists, + export_raster_asset_to_gee, +) + + +def generate_drought_resistance( + aez, start_year=2004, end_year=None, gee_account_id=None +): + """ + * Forest Sensitivity Analysis Pipeline — Script 2 + * Drought Resistance & Resilience (Harmonized kNDVI + Signed Formulas) + * + * For each forest pixel, computes mean resistance and resilience + * across all drought years (SPEI-12 < threshold). + * + * Harmonization: Transforms Landsat 8/9 (OLI) to Landsat 5/7 (ETM+) + * equivalent before computing kNDVI to eliminate sensor-shift bias. + * + * Signed resistance (both +ve and -ve events): + * Resistance = Yn_bar / |Ye - Yn_bar| × sign(Ye - Yn_bar) + * + * Resilience computed ONLY when Ye < Yn_bar (negative effect years): + * Resilience = |Ye - Yn_bar| / |Ye+1 - Yn_bar| × sign(Ye+1 - Yn_bar) + * + * Requires: + * - Forest mask asset from Script 1 + * - SPEI-12 assets from spei-drought-analysis-pipeline + """ + + ee_initialize(gee_account_id) + + TREE_COVER_ASSET = f"projects/corestack-datasets-alpha/assets/datasets/SPEI/Hybrid_Tree_AEZ_{aez}_{str(2003)}_{str(end_year)}" + OUTPUT_DESC = f"Drought_Metrics_AEZ_{aez}" + OUTPUT_ASSET_ID = ( + f"projects/corestack-datasets-alpha/assets/datasets/SPEI/{OUTPUT_DESC}" + ) + + if is_gee_asset_exists(OUTPUT_ASSET_ID): + return None + + DROUGHT_THRESHOLD = -1.0 # SPEI-12 below this = drought year + + # Fixed baseline window. This is independent of analysis START_YEAR/END_YEAR. + # Please don't EVER change this once results are published, or old outputs will change when the pipeline timeline is extended. + # This helps in fixing the Yn_bar (the average of the non-drought year NDVI) to a constant value. + BASELINE_START_YEAR = 2004 # SPEI has no data before 2004 + BASELINE_END_YEAR = 2024 + + aoi = ee.FeatureCollection(AEZ).filter(ee.Filter.eq("ae_regcode", aez)).geometry() + # aoi = ( + # ee.FeatureCollection("projects/ext-datasets/assets/datasets/State_pan_india") + # .filter(ee.Filter.eq("Name", "Odisha")) + # .geometry() + # ) + # Loading the assets := + + treeMeta = ee.Image(TREE_COVER_ASSET) + + startYear = treeMeta.select("start_year") + endYear = treeMeta.select("end_year") + + # Load SPEI-12 collection from single multiband asset + SPEI12_ASSET = f"projects/corestack-datasets-alpha/assets/datasets/SPEI/SPEI12" + spei12_raw = ee.Image(SPEI12_ASSET) + spei12_bandnames = [] + + for yn in range(2004, end_year + 1): + spei12_bandnames.append("y" + str(yn)) + spei12_named = spei12_raw.rename(spei12_bandnames) + + # Building the per-year SPEI collection here. + speiMinYear = min(start_year, BASELINE_START_YEAR) + speiMaxYear = max(end_year, BASELINE_END_YEAR) + + speiImages = [] + for y in range(speiMinYear, speiMaxYear + 1): + speiImages.append( + spei12_named.select("y" + str(y)).rename("spei").set("year", y) + ) + speiCol = ee.ImageCollection(speiImages) + + # LANDSAT HARMONIZATION & kNDVI := + + # Chastain et al. coefficients (OLI to ETM+) + chastainBandNames = ["BLUE", "GREEN", "RED", "NIR", "SWIR1", "SWIR2"] + oliETMSlopes = ee.Image.constant( + [1.03501, 1.00921, 1.01991, 1.14061, 1.04351, 1.05271] + ) + oliETMIntercepts = ee.Image.constant( + [-0.0055, -0.0008, -0.0021, -0.0163, -0.0045, 0.00261] + ) + + # Pre-process Landsat 5/7 (Baseline) + def prepL57(image): + qa = image.select("QA_PIXEL") + mask = qa.bitwiseAnd(1 << 3).eq(0).And(qa.bitwiseAnd(1 << 4).eq(0)) + + # Apply mask, select optical bands, and apply Collection 2 scale factors + scaled = ( + image.updateMask(mask) + .select(["SR_B1", "SR_B2", "SR_B3", "SR_B4", "SR_B5", "SR_B7"]) + .multiply(0.0000275) + .add(-0.2) + ) + + return scaled.rename(chastainBandNames).copyProperties( + image, ["system:time_start"] + ) + + # Pre-process Landsat 8/9 and Harmonize to ETM+ + def prepL89(image): + qa = image.select("QA_PIXEL") + mask = qa.bitwiseAnd(1 << 3).eq(0).And(qa.bitwiseAnd(1 << 4).eq(0)) + + # Apply mask, select optical bands, and apply Collection 2 scale factors + scaled = ( + image.updateMask(mask) + .select(["SR_B2", "SR_B3", "SR_B4", "SR_B5", "SR_B6", "SR_B7"]) + .multiply(0.0000275) + .add(-0.2) + .rename(chastainBandNames) + ) + + # Apply Chastain regression model (OLI -> ETM+) + harmonized = scaled.multiply(oliETMSlopes).add(oliETMIntercepts) + + return harmonized.copyProperties(image, ["system:time_start"]) + + # Calculate annual median kNDVI + def getAnnualKNDVI(year): + start = ee.Date.fromYMD(year, 1, 1) + end = ee.Date.fromYMD(year, 12, 31) + + l89 = ( + ee.ImageCollection("LANDSAT/LC08/C02/T1_L2") + .merge(ee.ImageCollection("LANDSAT/LC09/C02/T1_L2")) + .filterDate(start, end) + .filterBounds(aoi) + .map(prepL89) + .map( + lambda img: img.normalizedDifference(["NIR", "RED"]) + .pow(2) + .tanh() + .rename("kndvi") + ) + ) + + l57 = ( + ee.ImageCollection("LANDSAT/LT05/C02/T1_L2") + .merge(ee.ImageCollection("LANDSAT/LE07/C02/T1_L2")) + .filterDate(start, end) + .filterBounds(aoi) + .map(prepL57) + .map( + lambda img: img.normalizedDifference(["NIR", "RED"]) + .pow(2) + .tanh() + .rename("kndvi") + ) + ) + + return l89.merge(l57).median().set("year", year).rename("kndvi") + + # Load kNDVI for START_YEAR to END_YEAR+1 (need next year for resilience) + # kndviYears = ee.List.sequence(start_year, end_year + 1) + kndviMinYear = min(start_year, BASELINE_START_YEAR) + kndviMaxYear = max(end_year + 1, BASELINE_END_YEAR) + + kndviYears = ee.List.sequence(kndviMinYear, kndviMaxYear) + kndviCol = ee.ImageCollection(kndviYears.map(getAnnualKNDVI)) + + # BASELINE kNDVI (Yn_bar) := + # Mean kNDVI across non-drought years only + + # The analysis years will still remain the same , so if some internal year width is given like 2010-2018 for example + # then too the baseline yn_bar would be same of the bigger normalization. But the analysis will be resulting only of the analysis years. + analysisYears = ee.List.sequence(start_year, end_year) + + baselineYears = ee.List.sequence(BASELINE_START_YEAR, BASELINE_END_YEAR) + + def calc_kndviNonDrought(y): + year = ee.Number(y) + kndvi = kndviCol.filter(ee.Filter.eq("year", year)).first() + spei = ( + speiCol.filter(ee.Filter.eq("year", year)) + .first() + .resample("bilinear") + .reproject(crs=kndvi.projection(), scale=30) + ) + isNonDrought = spei.gte(DROUGHT_THRESHOLD) + return kndvi.updateMask(isNonDrought).set("year", year) + + kndviNonDrought = ee.ImageCollection(baselineYears.map(calc_kndviNonDrought)) + + Yn_bar = kndviNonDrought.mean().rename("kndvi_baseline") + + # SIGNED RESISTANCE & RESILIENCE := + + def calc_metrics_col(y): + year = ee.Number(y) + + kndviYe = kndviCol.filter(ee.Filter.eq("year", year)).first() + speiYe = ( + speiCol.filter(ee.Filter.eq("year", year)) + .first() + .resample("bilinear") + .reproject(crs=kndviYe.projection(), scale=30) + ) + + # Only compute on forest pixels during drought years + isForest = startYear.lte(year).And(endYear.gte(year)) + isDrought = speiYe.lt(DROUGHT_THRESHOLD) + eventMask = isForest.And(isDrought) + + diffRaw = kndviYe.subtract(Yn_bar) + diffAbs = diffRaw.abs().max(1e-6) + + # Resistance: signed, computed for ALL drought years + resistance = ( + Yn_bar.divide(diffAbs) + .multiply(diffRaw.signum()) + .rename("resistance") + .updateMask(eventMask) + ) + + # Resilience: ONLY computed when Ye < Yn_bar (negative effect years) + isNegativeEffect = kndviYe.lt(Yn_bar) + resilMask = eventMask.And(isNegativeEffect) + + kndviNext = kndviCol.filter(ee.Filter.eq("year", year.add(1))).first() + diffNext = kndviNext.subtract(Yn_bar) + diffNextAbs = diffNext.abs().max(1e-6) + + resilience = ( + diffAbs.divide(diffNextAbs) + .multiply(diffNext.signum()) + .rename("resilience") + .updateMask(resilMask) + ) + + return ee.Image.cat([resistance, resilience]).set("year", year) + + metricsCol = ee.ImageCollection(analysisYears.map(calc_metrics_col)) + + # AGGREGATE & EXPORT := + + meanResistance = metricsCol.select("resistance").mean().clip(aoi) + meanResilience = metricsCol.select("resilience").mean().clip(aoi) + + finalOutput = meanResistance.rename("resistance").addBands( + meanResilience.rename("resilience") + ) + + task_id = export_raster_asset_to_gee( + finalOutput, OUTPUT_DESC, OUTPUT_ASSET_ID, scale=30, region=aoi + ) + + return task_id diff --git a/computing/spei/forestfire_sensitivity/export_fire_index.py b/computing/spei/forestfire_sensitivity/export_fire_index.py new file mode 100644 index 00000000..2200f47b --- /dev/null +++ b/computing/spei/forestfire_sensitivity/export_fire_index.py @@ -0,0 +1,173 @@ +import ee + +from utilities.constants import AEZ +from utilities.gee_utils import ( + export_raster_asset_to_gee, + ee_initialize, + is_gee_asset_exists, +) + + +def fire_index(aez, start_year=2004, end_year=2022, gee_account_id=None): + """ + * Forest Sensitivity Analysis Pipeline — Fire Script + * Fire Radiative Power (FRP) Index Export (Threshold > 30) + * + * Computes 5 quantities per pixel per year and exports as a single + * multiband asset — one band per year for each quantity: + * + * FRP_{year} = annual sum of FRP on fire days + * zScore_{year} = z-score of FRP relative to the 2004-2022 period + * fireDays_{year} = number of days in the year with FRP > 30 + * fireAvg_{year} = average daily FRP on fire days + * maxFRP_{year} = maximum daily FRP recorded in that year + * + * Fire day definition: daily MaxFRP > 30 (MODIS Terra Thermal Anomalies) + * + * Requires: nothing — only public datasets (MODIS MOD14A1) + */ + """ + ee_initialize(gee_account_id) + OUTPUT_DESC = f"fire_index_FRP30_AEZ_{aez}" + OUTPUT_ASSET_ID = ( + f"projects/corestack-datasets-alpha/assets/datasets/SPEI/{OUTPUT_DESC}" + ) + + if is_gee_asset_exists(OUTPUT_ASSET_ID): + return None + + aoi = ee.FeatureCollection(AEZ).filter(ee.Filter.eq("ae_regcode", aez)).geometry() + # aoi = ( + # ee.FeatureCollection("projects/ext-datasets/assets/datasets/State_pan_india") + # .filter(ee.Filter.eq("Name", "Odisha")) + # .geometry() + # ) + FRP_THRESHOLD = 30 + """ + Fixed baseline window for zScore normalization — independent of START_YEAR/END_YEAR. + This is matching the same 2004-2024 window locked in for rain and drought, so all z-score-based indices + are normalized against the same reference period. + Do NOT change this once results are published, or old zScore bands will drift when we extend the analysis timeline later. + """ + BASELINE_START_YEAR = 2004 + BASELINE_END_YEAR = 2024 + + # Terra Thermal Anomalies & Fire Daily 1km + modisFire = ( + ee.ImageCollection("MODIS/061/MOD14A1").filterBounds(aoi).select("MaxFRP") + ) + + proj = modisFire.first().projection() + + # 1. ANNUAL METRICS CALCULATION + # We need annual metrics computed for every year covering BOTH the analysis window and the baseline window, + # whichever stretches further. + metricsMinYear = min(start_year, BASELINE_START_YEAR) + metricsMaxYear = max(end_year, BASELINE_END_YEAR) + metricsYears = ee.List.sequence(metricsMinYear, metricsMaxYear) + + def annual_collection(y): + start = ee.Date.fromYMD(y, 1, 1) + end = ee.Date.fromYMD(y, 12, 31) + + yearCollection = modisFire.filterDate(start, end) + + # Absolute maximum daily FRP for this year + maxFRP = ( + yearCollection.max().unmask(0).setDefaultProjection(proj).rename("maxFRP") + ) + + # Binary mask: isolate days where FRP > 30 + fireDaysCollection = yearCollection.map( + lambda img: img.updateMask(img.gt(FRP_THRESHOLD)) + ) + + # 1. Annual sum of FRP on fire days + sumFRP = ( + fireDaysCollection.map(lambda img: img.unmask(0)) + .sum() + .setDefaultProjection(proj) + .rename("FRP") + ) + + # 2. Number of fire days + fireDays = ( + fireDaysCollection.map(lambda img: img.mask()) + .sum() + .unmask(0) + .setDefaultProjection(proj) + .rename("fireDays") + ) + + # 3. Average FRP of fire days + fireAvg = ( + fireDaysCollection.mean() + .unmask(0) + .setDefaultProjection(proj) + .rename("fireAvg") + ) + + # Combine metrics into a single image per year containing all 4 base properties + return ( + sumFRP.addBands(fireDays).addBands(fireAvg).addBands(maxFRP).set("year", y) + ) + + annualMetrics = ee.ImageCollection(metricsYears.map(annual_collection)) + + # 2. FIXED Z-SCORE CALCULATION + # Baseline stats only from the frozen baseline years, not from whatever analysis window I happen to be running right now. + baselineYears = ee.List.sequence(BASELINE_START_YEAR, BASELINE_END_YEAR) + + baselineMetrics = annualMetrics.filter(ee.Filter.inList("year", baselineYears)) + + frpMean = baselineMetrics.select("FRP").mean() + frpStdDev = baselineMetrics.select("FRP").reduce(ee.Reducer.stdDev()) + + # Server-side map architecture over the annual image collection + def completed_annual_collection_func(img): + frp = img.select("FRP") + z = frp.subtract(frpMean).divide(frpStdDev).rename("zScore") + return img.addBands(z) + + completedAnnualCollection = annualMetrics.map(completed_annual_collection_func) + + # 3. SERVER-SIDE STACK INTO SINGLE MULTIBAND IMAGE & EXPORT + analysisYears = ee.List.sequence(start_year, end_year) + + def add_bands_for_year(y, acc): + yearStr = ee.String(ee.Number(y).toInt()) + yearImg = completedAnnualCollection.filter(ee.Filter.eq("year", y)).first() + + frpBand = yearImg.select("FRP").rename(ee.String("FRP_").cat(yearStr)) + zBand = yearImg.select("zScore").rename(ee.String("zScore_").cat(yearStr)) + fireDaysBand = yearImg.select("fireDays").rename( + ee.String("fireDays_").cat(yearStr) + ) + fireAvgBand = yearImg.select("fireAvg").rename( + ee.String("fireAvg_").cat(yearStr) + ) + maxFRPBand = yearImg.select("maxFRP").rename(ee.String("maxFRP_").cat(yearStr)) + + return ( + ee.Image(acc) + .addBands(frpBand) + .addBands(zBand) + .addBands(fireDaysBand) + .addBands(fireAvgBand) + .addBands(maxFRPBand) + ) + + empty_image = ee.Image().mask(ee.Image(0)) + output_image = ee.Image(analysisYears.iterate(add_bands_for_year, empty_image)) + + output_image = output_image.select(output_image.bandNames().remove("constant")) + + # Export execution block + task_id = export_raster_asset_to_gee( + output_image.clip(aoi), OUTPUT_DESC, OUTPUT_ASSET_ID, scale=1000, region=aoi + ) + + print("✅ Clean pipeline compilation verified.") + print("Ready to execute in the tasks tab. Total structured bands: 95.") + + return task_id diff --git a/computing/spei/forestfire_sensitivity/forest_fire_resistance_resilience.py b/computing/spei/forestfire_sensitivity/forest_fire_resistance_resilience.py new file mode 100644 index 00000000..186b0272 --- /dev/null +++ b/computing/spei/forestfire_sensitivity/forest_fire_resistance_resilience.py @@ -0,0 +1,253 @@ +import ee + +from utilities.constants import AEZ +from utilities.gee_utils import export_raster_asset_to_gee, is_gee_asset_exists + +""" + * Forest Sensitivity Analysis Pipeline — Fire Resistance & Resilience + * Fire Shock Resistance & Resilience (Harmonized kNDVI + Signed Formulas) + * + * Signed resistance (both +ve and -ve events): + * Resistance = Yn_bar / |Ye - Yn_bar| × sign(Ye - Yn_bar) + * + * Resilience computed ONLY when Ye < Yn_bar (negative effect years): + * Resilience = |Ye - Yn_bar| / |Ye+1 - Yn_bar| × sign(Ye+1 - Yn_bar) + * + * Harmonization: Transforms Landsat 8/9 (OLI) to Landsat 5/7 (ETM+) + * equivalent before computing kNDVI to eliminate sensor-shift bias. + * + * Requires: + * - Forest mask asset (Script 1) + * - Fire index asset (FRP > 30) +""" + + +def forest_fire_sensitivity(aez, start_year=2004, end_year=2022, gee_account_id=None): + TREE_COVER_ASSET = f"projects/corestack-datasets-alpha/assets/datasets/SPEI/Hybrid_Tree_AEZ_{aez}_{str(2003)}_{str(end_year)}" + + FIRE_INDEX_ASSET = f"projects/corestack-datasets-alpha/assets/datasets/SPEI/fire_index_FRP30_AEZ_{aez}" + + OUTPUT_DESC = f"fire_metrics_harmonized_kNDVI_AEZ_{aez}" + OUTPUT_ASSET_ID = ( + f"projects/corestack-datasets-alpha/assets/datasets/SPEI/{OUTPUT_DESC}" + ) + + if is_gee_asset_exists(OUTPUT_ASSET_ID): + return None + + Z_THRESHOLD = 1.0 + + # I'm using 2004-2024 to match the same baseline window I already locked + # in for drought (Script 2), rain (Script 3b), and the fire z-score + # itself (fire index script) — keeping all my baselines consistent with + # each other. + BASELINE_START_YEAR = 2004 + BASELINE_END_YEAR = 2024 + + aoi = ee.FeatureCollection(AEZ).filter(ee.Filter.eq("ae_regcode", aez)).geometry() + # aoi = ( + # ee.FeatureCollection("projects/ext-datasets/assets/datasets/State_pan_india") + # .filter(ee.Filter.eq("Name", "Odisha")) + # .geometry() + # ) + # Loading the assets := + treeMeta = ee.Image(TREE_COVER_ASSET) + startYearTree = treeMeta.select("start_year") + endYearTree = treeMeta.select("end_year") + + fireIndex = ee.Image(FIRE_INDEX_ASSET) + + # I need zScore bands covering BOTH my analysis window and my baseline + # window — whichever stretches further in either direction. Right now + # they're the same range (2004-2024), so this doesn't change anything + # today, but it protects me for later when I extend END_YEAR and the two + # windows stop lining up. + zMinYear = min(start_year, BASELINE_START_YEAR) + zMaxYear = max(end_year, BASELINE_END_YEAR) + + zScoreCol_list = [] + + for y in range(zMinYear, zMaxYear + 1): + zScoreCol_list.append( + fireIndex.select("zScore_" + str(y)).rename("zScore").set("year", y) + ) + + zScoreCol = ee.ImageCollection(zScoreCol_list) + + # LANDSAT HARMONIZATION & kNDVI := + chastainBandNames = ["BLUE", "GREEN", "RED", "NIR", "SWIR1", "SWIR2"] + oliETMSlopes = ee.Image.constant( + [1.03501, 1.00921, 1.01991, 1.14061, 1.04351, 1.05271] + ) + oliETMIntercepts = ee.Image.constant( + [-0.0055, -0.0008, -0.0021, -0.0163, -0.0045, 0.00261] + ) + + # Pre-process Landsat 5/7 (Baseline) + def prepL57(image): + qa = image.select("QA_PIXEL") + mask = qa.bitwiseAnd(1 << 3).eq(0).And(qa.bitwiseAnd(1 << 4).eq(0)) + + scaled = ( + image.updateMask(mask) + .select(["SR_B1", "SR_B2", "SR_B3", "SR_B4", "SR_B5", "SR_B7"]) + .multiply(0.0000275) + .add(-0.2) + ) + + return scaled.rename(chastainBandNames).copyProperties( + image, ["system:time_start"] + ) + + # Pre-process Landsat 8/9 and Harmonize to ETM+ + def prepL89(image): + qa = image.select("QA_PIXEL") + mask = qa.bitwiseAnd(1 << 3).eq(0).And(qa.bitwiseAnd(1 << 4).eq(0)) + + scaled = ( + image.updateMask(mask) + .select(["SR_B2", "SR_B3", "SR_B4", "SR_B5", "SR_B6", "SR_B7"]) + .multiply(0.0000275) + .add(-0.2) + .rename(chastainBandNames) + ) + + harmonized = scaled.multiply(oliETMSlopes).add(oliETMIntercepts) + return harmonized.copyProperties(image, ["system:time_start"]) + + # Calculate annual median kNDVI + def getAnnualKNDVI(year): + start = ee.Date.fromYMD(year, 1, 1) + end = ee.Date.fromYMD(year, 12, 31) + + l89 = ( + ee.ImageCollection("LANDSAT/LC08/C02/T1_L2") + .merge(ee.ImageCollection("LANDSAT/LC09/C02/T1_L2")) + .filterDate(start, end) + .filterBounds(aoi) + .map(prepL89) + .map( + lambda img: img.normalizedDifference(["NIR", "RED"]) + .pow(2) + .tanh() + .rename("kndvi") + ) + ) + + l57 = ( + ee.ImageCollection("LANDSAT/LT05/C02/T1_L2") + .merge(ee.ImageCollection("LANDSAT/LE07/C02/T1_L2")) + .filterDate(start, end) + .filterBounds(aoi) + .map(prepL57) + .map( + lambda img: img.normalizedDifference(["NIR", "RED"]) + .pow(2) + .tanh() + .rename("kndvi") + ) + ) + + return l89.merge(l57).median().set("year", year).rename("kndvi") + + # Same idea as zScoreCol above — I need kNDVI images covering whichever + # is bigger: my baseline window, or my analysis window PLUS ONE year + # (because resilience for my very last analysis year needs next-year + # kNDVI to compare against). + kndviMinYear = min(start_year, BASELINE_START_YEAR) + kndviMaxYear = max(end_year + 1, BASELINE_END_YEAR) + kndviYears = ee.List.sequence(kndviMinYear, kndviMaxYear) + kndviCol = ee.ImageCollection(kndviYears.map(getAnnualKNDVI)) + + # BASELINE kNDVI (Yn_bar):= + # Mean kNDVI across non-anomalous years only + + analysisYears = ee.List.sequence(start_year, end_year) + + # This is my frozen baseline window — the years I use to WORK OUT what + # Yn_bar (my "normal" kNDVI reference) is. On purpose, kept separate from + # analysisYears so extending my results later never shifts this. + baselineYears = ee.List.sequence(BASELINE_START_YEAR, BASELINE_END_YEAR) + + def calc_kndvi(y): + year = ee.Number(y) + kndvi = ee.Image(kndviCol.filter(ee.Filter.eq("year", year)).first()) + zScore = ( + ee.Image(zScoreCol.filter(ee.Filter.eq("year", year)).first()) + .resample("bilinear") + .reproject(crs=kndvi.projection(), scale=30) + ) + + isNormal = zScore.select("zScore").abs().lt(Z_THRESHOLD) + isForest = startYearTree.lte(year).And(endYearTree.gte(year)) + + return kndvi.updateMask(isNormal.And(isForest)).set("year", year) + + Yn_bar = ( + ee.ImageCollection(baselineYears.map(calc_kndvi)) + .mean() + .rename("kndvi_baseline") + ) + + # SIGNED RESISTANCE & RESILIENCE := + def calc_kndvi_ye(y): + + year = ee.Number(y) + + kndviYe = ee.Image(kndviCol.filter(ee.Filter.eq("year", year)).first()) + zScore = ( + ee.Image(zScoreCol.filter(ee.Filter.eq("year", year)).first()) + .resample("bilinear") + .reproject(crs=kndviYe.projection(), scale=30) + ) + + # Only compute on forest pixels during anomalous fire years + isAnomalous = zScore.select("zScore").gt(Z_THRESHOLD) + isForest = startYearTree.lte(year).And(endYearTree.gte(year)) + eventMask = isAnomalous.And(isForest) + + diffRaw = kndviYe.subtract(Yn_bar) + diffAbs = diffRaw.abs().max(1e-6) + + # Resistance: signed, computed for ALL anomalous years (both +ve and -ve) + resistance = ( + Yn_bar.divide(diffAbs) + .multiply(diffRaw.signum()) + .rename("resistance") + .updateMask(eventMask) + ) + + # Resilience: ONLY computed when Ye < Yn_bar (negative effect years) + isNegativeEffect = kndviYe.lt(Yn_bar) + resil_mask = eventMask.And(isNegativeEffect) + + kndvi_next = ee.Image( + kndviCol.filter(ee.Filter.eq("year", year.add(1))).first() + ) + diffNext = kndvi_next.subtract(Yn_bar) + diffNextAbs = diffNext.abs().max(1e-6) + + resilience = ( + diffAbs.divide(diffNextAbs) + .multiply(diffNext.signum()) + .rename("resilience") + .updateMask(resil_mask) + ) + + return ee.Image.cat([resistance, resilience]).set("year", year) + + metricsCol = ee.ImageCollection(analysisYears.map(calc_kndvi_ye)) + + # AGGREGATE & EXPORT := + mean_resist = metricsCol.select("resistance").mean().clip(aoi) + mean_resil = metricsCol.select("resilience").mean().clip(aoi) + + finalOutput = mean_resist.rename("resistance").addBands( + mean_resil.rename("resilience") + ) + + task_id = export_raster_asset_to_gee( + finalOutput, OUTPUT_DESC, OUTPUT_ASSET_ID, scale=30, region=aoi + ) + + return task_id diff --git a/computing/spei/generate_spei/__init__.py b/computing/spei/generate_spei/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/computing/spei/generate_spei/compute_spei.R b/computing/spei/generate_spei/compute_spei.R new file mode 100644 index 00000000..b6ced39f --- /dev/null +++ b/computing/spei/generate_spei/compute_spei.R @@ -0,0 +1,225 @@ + +# SPEI Pipeline +# Read multiband P-PET GeoTIFF, compute SPEI-1/3/12 pixel-wise, +# write 3 multiband output GeoTIFFs with named bands. +# Here the reference baseline period is taken as 2004-2023. +# Change the end_year variable to whatever year you wanna extend the pipeline to. +# If it is not intentional, don't touch the ref_start and ref_end variables for +# extending the pipeline as it will change the SPEI values for all previous years too. +# Run below commands for installing the dependencies if not already installed: +# sudo apt install r-base-core +# conda install -c conda-forge r-spei r-raster +# Rscript -e "install.packages('terra', repos='https://cloud.r-project.org')" +# Rscript -e "install.packages('raster', repos='https://cloud.r-project.org')" + +library(SPEI) +library(raster) +library(terra) + +run_spei_pipeline <- function(aez, start_year, end_year) { + + input_file <- paste0("data/base_layers/spei/inputs/", aez, "/monthly/P_PET_AEZ_", aez, "_monthly_multiband.tif") + # output_dir <- paste0("data/base_layers/spei/outputs") + + # if (!dir.exists(output_dir)) dir.create(output_dir, recursive = TRUE) + + output_base <- "data/base_layers/spei/outputs" + + output_dir1 <- file.path(output_base, "SPEI_1") + output_dir3 <- file.path(output_base, "SPEI_3") + output_dir12 <- file.path(output_base, "SPEI_12") + + dir.create(output_dir1, recursive = TRUE, showWarnings = FALSE) + dir.create(output_dir3, recursive = TRUE, showWarnings = FALSE) + dir.create(output_dir12, recursive = TRUE, showWarnings = FALSE) + + # --- YEAR RANGE --- + ref_start <- 2004 # baseline period start — distribution fitted on this range + ref_end <- 2023 # baseline period end — freeze this when extending to future years + + n_years <- end_year - start_year + 1 + n_monthly <- n_years * 12 + n_seasonal <- n_years * 4 + n_annual <- n_years + n_output <- n_monthly + n_seasonal + n_annual + + # --- Resume check --- + # out_check <- file.path(output_dir, paste0("SPEI12_", aez, ".tif")) + # if (file.exists(out_check)) { + # stop(paste("Already processed:", aez, "— delete output files to rerun.")) + # } + + # ============================================================================= + # SPEI FUNCTION — do not modify + # Input: variable-length P-PET time series + # Output: variable-length vector (SPEI-1 all: 12*(no. of years), SPEI-3 seasonal: 4*(no. of years), SPEI-12 annual: no. of years) + # ============================================================================= + spei_function <- function(x, ...) { + tryCatch({ + if (all(is.na(x))) return(rep(NA, n_output)) + + pixel_ts <- ts(x, start = c(start_year, 1), frequency = 12) + + spei1_all <- as.vector(spei(pixel_ts, 1, + distribution = 'log-Logistic', + ref.start = c(ref_start, 1), + ref.end = c(ref_end, 12), + na.rm = TRUE)$fitted) + spei3_all <- as.vector(spei(pixel_ts, 3, + distribution = 'log-Logistic', + ref.start = c(ref_start, 1), + ref.end = c(ref_end, 12), + na.rm = TRUE)$fitted) + spei12_all <- as.vector(spei(pixel_ts, 12, + distribution = 'log-Logistic', + ref.start = c(ref_start, 1), + ref.end = c(ref_end, 12), + na.rm = TRUE)$fitted) + + if (length(spei1_all) != n_monthly) stop("Incorrect output length.") + + seasonal_idx <- which(((seq_along(spei3_all) - 1) %% 12 + 1) %in% c(3,6,9,12)) + annual_idx <- seq(12, n_monthly, by = 12) + + c(spei1_all, spei3_all[seasonal_idx], spei12_all[annual_idx]) + + }, error = function(e) rep(NA, n_output)) + } + + # --- Load input --- + cat(paste("Loading:", input_file, "\n")) + p_pet_brick <- brick(input_file) + cat(paste("Loaded", nlayers(p_pet_brick), "bands (expected", n_monthly, ")\n")) + + # --- Band names --- + agri_months <- c(7:12, 1:6) + + years <- unlist(lapply(start_year:end_year, function(y) { + c(rep(y, 6), rep(y + 1, 6)) + })) + + spei1_names <- paste0( + "y", + years, + "_m", + sprintf("%02d", rep(agri_months, n_years)) + ) + + agri_quarter_months <- c("07_09", "10_12", "01_03", "04_06") + + years3 <- unlist(lapply(start_year:end_year, function(y) { + c(y, y, y + 1, y + 1) + })) + + spei3_names <- paste0( + "y", + years3, + "_m", + sprintf("%s", rep(agri_quarter_months, n_years)) + ) + + spei12_names <- paste0( + start_year:end_year, + "_", + (start_year + 1):(end_year + 1) + ) + + # --- Compute block by block --- + cat("Running SPEI computation...\n") + temp_file <- file.path(output_base, paste0(aez, "_temp.tif")) + result_brick <- brick(p_pet_brick, nl = n_output) + result_brick <- writeStart(result_brick, filename = temp_file, overwrite = TRUE) + + bs <- blockSize(p_pet_brick) + for (i in 1:bs$n) { + v <- getValues(p_pet_brick, row = bs$row[i], nrows = bs$nrows[i]) + res <- t(apply(v, 1, spei_function)) + writeValues(result_brick, res, bs$row[i]) + if (i %% 5 == 0) cat(paste(" Chunk", i, "/", bs$n, "\n")) + } + result_brick <- writeStop(result_brick) + cat("Computation complete.\n") + + # --- Split and save --- + cat("Saving output files...\n") + all_b <- brick(temp_file) + + spei1_end <- n_monthly + spei3_end <- n_monthly + n_seasonal + + spei1_b <- all_b[[1:spei1_end]] + spei3_b <- all_b[[(spei1_end + 1):spei3_end]] + spei12_b <- all_b[[(spei3_end + 1):n_output]] + + names(spei1_b) <- spei1_names + names(spei3_b) <- spei3_names + names(spei12_b) <- spei12_names + + spei1_file <- file.path(output_dir1, paste0("SPEI1_", aez, ".tif")) + spei3_file <- file.path(output_dir3, paste0("SPEI3_", aez, ".tif")) + spei12_file <- file.path(output_dir12, paste0("SPEI12_", aez, ".tif")) + + if (file.exists(spei1_file)) file.remove(spei1_file) + if (file.exists(spei3_file)) file.remove(spei3_file) + if (file.exists(spei12_file)) file.remove(spei12_file) + + spei1_t <- rast(spei1_b) + names(spei1_t) <- spei1_names + + terra::writeRaster( + spei1_t, + spei1_file, + overwrite = TRUE, + NAflag = -9999, + gdal = c("COMPRESS=LZW") + ) + + r <- terra::rast(spei1_file) + + spei3_t <- rast(spei3_b) + names(spei3_t) <- spei3_names + + terra::writeRaster( + spei3_t, + spei3_file, + overwrite = TRUE, + NAflag = -9999 + ) + + spei12_t <- rast(spei12_b) + names(spei12_t) <- spei12_names + + terra::writeRaster( + spei12_t, + spei12_file, + overwrite = TRUE, + NAflag = -9999 + ) + + file.remove(temp_file) + + cat("\nDone.\n") + cat(paste0("SPEI-1 : ", spei1_file, "\n")) + cat(paste0("SPEI-3 : ", spei3_file, "\n")) + cat(paste0("SPEI-12 : ", spei12_file, "\n")) + + cat(paste0(" SPEI1_", aez, ".tif — ", nlayers(spei1_b), " bands\n")) + cat(paste0(" SPEI3_", aez, ".tif — ", nlayers(spei3_b), " bands\n")) + cat(paste0(" SPEI12_", aez, ".tif — ", nlayers(spei12_b), " bands\n")) +} + + +# ============================================================================= +# FUNCTION CALL +# ============================================================================= +args <- commandArgs(trailingOnly = TRUE) + +if (length(args) < 1) { + stop("AEZ argument is required") +} + +aez <- args[1] +start_year <- as.integer(args[2]) +end_year <- as.integer(args[3]) + +run_spei_pipeline(aez, start_year, end_year) \ No newline at end of file diff --git a/computing/spei/generate_spei/download_base_datasets.py b/computing/spei/generate_spei/download_base_datasets.py new file mode 100644 index 00000000..0225816f --- /dev/null +++ b/computing/spei/generate_spei/download_base_datasets.py @@ -0,0 +1,444 @@ +""" +Download drought inputs locally from Google Earth Engine. + +This script downloads pan-India or single-state GeoTIFFs locally for: + - UCSB-CHG/CHIRPS/DAILY precipitation + - MODIS/061/MOD16A2GF PET + +It downloads one small GeoTIFF per time step for each dataset. + +Examples: + main(aoi="india", datasets=["both"]) + main(aoi="india", datasets=["chirps", "modis_pet"], start_date="2004-01-01", end_date="2023-12-31") + main(aoi="state", state="Madhya Pradesh", datasets=["chirps"]) +""" + +from __future__ import annotations + +import time +from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import datetime, timedelta +from pathlib import Path + +import ee +import requests + +from utilities.constants import AEZ + +CHIRPS_COLLECTION = "UCSB-CHG/CHIRPS/DAILY" +MODIS_PET_COLLECTION = "MODIS/061/MOD16A2GF" +DATASET_CHOICES = ("chirps", "modis_pet", "both") + + +def date_range( + start_date: ee.Date, end_date_exclusive: ee.Date, unit: str +) -> list[ee.Date]: + # Builds monthly/daily date anchors. end_date_exclusive is intentionally + # advanced by one day in build_dataset_image so user input is inclusive. + count = end_date_exclusive.difference(start_date, unit).round().getInfo() + return [start_date.advance(offset, unit) for offset in range(count)] + + +def collection_dates( + collection: ee.ImageCollection, start_date: ee.Date, end_date_exclusive: ee.Date +) -> list[ee.Date]: + # Native cadence should use the source image timestamps. This is important + # for MODIS PET, which is 8-day rather than daily. + millis = ( + collection.filterDate(start_date, end_date_exclusive) + .aggregate_array("system:time_start") + .getInfo() + ) + return [ee.Date(value) for value in millis] + + +def expand_datasets(selected: list[str]) -> list[str]: + print("Inside expand_datasets") + if isinstance(selected, str): + selected = [selected] + if "both" in selected: + return ["chirps", "modis_pet"] + return selected + + +def make_chirps_image( + chirps: ee.ImageCollection, + aoi: ee.FeatureCollection, + start_date: ee.Date, + frequency: str, +) -> tuple[ee.Image, str]: + # CHIRPS is daily precipitation. Monthly mode sums daily precipitation + # into one monthly image before local download. + if frequency in ("daily", "native"): + end_date = start_date.advance(1, "day") + label = start_date.format("YYYYMMdd").getInfo() + image = chirps.filterDate(start_date, end_date).first() + else: + end_date = start_date.advance(1, "month") + label = start_date.format("YYYYMM").getInfo() + image = chirps.filterDate(start_date, end_date).sum() + + image = ( + ee.Image(image) + .select("precipitation") + .rename("precipitation") + .clip(aoi) + .toFloat() + ) + return image, label + + +def make_modis_pet_image( + modis_pet: ee.ImageCollection, + aoi: ee.FeatureCollection, + start_date: ee.Date, + frequency: str, + target_projection: ee.Projection | None = None, +) -> tuple[ee.Image, str]: + # MOD16A2GF PET is not daily. The PET band has a 0.1 scale factor, applied + # here so downloaded rasters contain real PET values. + if frequency == "daily": + raise ValueError("MODIS PET is not daily. Use --frequency monthly or native.") + + if frequency == "native": + end_date = start_date.advance(8, "day") + label = start_date.format("YYYYMMdd").getInfo() + image = modis_pet.filterDate(start_date, end_date).first() + else: + end_date = start_date.advance(1, "month") + label = start_date.format("YYYYMM").getInfo() + image = modis_pet.filterDate(start_date, end_date).sum() + image = image.setDefaultProjection(modis_pet.first().projection()) + + image = ee.Image(image).multiply(0.1).rename("PET") + + if target_projection is not None: + # Match the original GEE P-PET script: + # PET.reduceResolution(mean).reproject(crs=P.projection()) + image = image.reduceResolution( + reducer=ee.Reducer.mean(), maxPixels=65536 + ).reproject(crs=target_projection) + + image = image.clip(aoi).toFloat() + return image, label + + +def download_image( + image: ee.Image, + aoi: ee.FeatureCollection, + output_path: Path, + scale: int, + crs: str | None = "EPSG:4326", + retries: int = 3, +) -> None: + # Earth Engine's direct download endpoint has a request-size limit, so this + # function downloads only one time step at a time. + params = { + "scale": scale, + "region": aoi.geometry(), + "format": "GEO_TIFF", + } + if crs: + params["crs"] = crs + + for attempt in range(1, retries + 1): + try: + # Generate a signed EE URL, then stream the GeoTIFF bytes to disk. + url = image.getDownloadURL(params) + with requests.get(url, stream=True, timeout=300) as response: + response.raise_for_status() + output_path.parent.mkdir(parents=True, exist_ok=True) + with output_path.open("wb") as handle: + for chunk in response.iter_content(chunk_size=1024 * 1024): + if chunk: + handle.write(chunk) + return + except Exception: + # Remove partial files so retries/reruns do not treat corrupt files + # as valid completed downloads. + if output_path.exists(): + output_path.unlink() + if attempt == retries: + raise + time.sleep(2 * attempt) + + +def get_last_downloaded_date( + output_dir: Path, aez: int, frequency: str, dataset: str +) -> str | None: + """ + Check the output directory and find the last downloaded date. + Returns the latest date as a string (YYYYMMDD or YYYYMM format). + """ + dataset_output_dir = output_dir / str(aez) / frequency / dataset + + if not dataset_output_dir.exists(): + return None + + # Get all .tif files in the directory + tif_files = list(dataset_output_dir.glob("*.tif")) + + if not tif_files: + return None + + # Extract dates from filenames (e.g., "CHIRPS_20240101.tif" or "CHIRPS_202401.tif") + dates = [] + for file_path in tif_files: + # Extract the date part from filename (last part before .tif) + filename = file_path.stem # e.g., "CHIRPS_20240101" + parts = filename.split("_") + if len(parts) >= 2: + date_str = parts[-1] # e.g., "20240101" + try: + # Validate it's a proper date format + if len(date_str) in (8, 6): # YYYYMMDD or YYYYMM + dates.append(date_str) + except (ValueError, IndexError): + continue + + if not dates: + return None + + # Sort and return the latest date + return sorted(dates)[-1] + + +def get_next_date(current_date: str, frequency: str) -> str: + """ + Given a downloaded-date label, return the next start date in the format + expected by Earth Engine's ee.Date parsing. + """ + if frequency in ("daily", "native"): + date_obj = datetime.strptime(current_date, "%Y%m%d") + next_date_obj = date_obj + timedelta(days=1) + return next_date_obj.strftime("%Y-%m-%d") + + date_obj = datetime.strptime(current_date, "%Y%m") + if date_obj.month == 12: + next_date_obj = date_obj.replace(year=date_obj.year + 1, month=1) + else: + next_date_obj = date_obj.replace(month=date_obj.month + 1) + return next_date_obj.strftime("%Y-%m-01") + + +def validate_inputs(aoi: str, datasets: list[str] | str, frequency: str) -> None: + # Keep validation near main() so notebook calls fail early and clearly. + print("Inside validate_inputs") + if aoi not in ("india", "state"): + raise ValueError("aoi must be 'india' or 'state'.") + if frequency not in ("daily", "monthly", "native"): + raise ValueError("frequency must be 'daily', 'monthly', or 'native'.") + + selected = [datasets] if isinstance(datasets, str) else datasets + invalid_datasets = set(selected) - set(DATASET_CHOICES) + if invalid_datasets: + raise ValueError(f"Invalid dataset(s): {sorted(invalid_datasets)}") + + +def dataset_label(dataset: str) -> str: + # Output labels are used in file names and final raster band descriptions. + if dataset == "chirps": + return "CHIRPS" + if dataset == "modis_pet": + return "MODIS_PET" + raise ValueError(f"Unknown dataset: {dataset}") + + +def build_dataset_image( + aez: int, + dataset: str, + start_date: str, + end_date: str, + frequency: str, +) -> tuple[ee.ImageCollection, ee.FeatureCollection, str, list[tuple[ee.Date, str]]]: + # Prepares the collection, AOI, and date labels for exactly one dataset. + # CHIRPS and MODIS PET stay separate from this point through final output. + start_ee_date = ee.Date(start_date) + end_date_exclusive = ee.Date(end_date).advance(1, "day") + + # region = get_aoi(aoi, state) + region = ee.FeatureCollection(AEZ).filter(ee.Filter.eq("ae_regcode", aez)) + chirps = ee.ImageCollection(CHIRPS_COLLECTION).select("precipitation") + modis_pet = ee.ImageCollection(MODIS_PET_COLLECTION).select("PET") + + if dataset == "chirps": + unit = "day" if frequency in ("daily", "native") else "month" + dates = date_range(start_ee_date, end_date_exclusive, unit) + collection = chirps + name = dataset_label(dataset) + else: + if frequency == "daily": + raise ValueError( + "MODIS PET is not daily. Use --frequency monthly or native." + ) + if frequency == "native": + dates = collection_dates(modis_pet, start_ee_date, end_date_exclusive) + else: + dates = date_range(start_ee_date, end_date_exclusive, "month") + collection = modis_pet + name = dataset_label(dataset) + + labeled_dates = [] + for date in dates: + # These labels become the downloaded GeoTIFF file names. + date_format = "YYYYMMdd" if frequency in ("daily", "native") else "YYYYMM" + label = date.format(date_format).getInfo() + labeled_dates.append((date, label)) + + print( + f"Preparing {len(labeled_dates)} {frequency} {name} image(s) " f"for aez {aez}" + ) + return collection, region, name, labeled_dates + + +def download_dataset_images( + aez: int, + dataset: str, + start_date: str, + end_date: str, + frequency: str, + output_dir: str, + scale: int, + crs: str | None, + sleep: float, + overwrite: bool, + max_workers: int, +) -> None: + # End-to-end local workflow for one dataset: + # download one small GeoTIFF per time step and keep those files on disk. + # aoi_label = "India" if aez == "india" else state + # safe_aoi = aoi_label.replace(" ", "_") + name = dataset_label(dataset) + print(output_dir, aez, frequency, dataset) + dataset_output_dir = Path(output_dir) / str(aez) / frequency / dataset + print("start_date", start_date) + # Check if files already exist and find the last downloaded date + last_downloaded = get_last_downloaded_date( + Path(output_dir), aez, frequency, dataset + ) + + if last_downloaded and not overwrite: + # Convert last downloaded date to next date + next_date_str = get_next_date(last_downloaded, frequency) + print(f"Last downloaded date: {last_downloaded}") + print(f"Resuming downloads from: {next_date_str}") + start_date = next_date_str + + collection, region, _, labeled_dates = build_dataset_image( + aez=aez, + dataset=dataset, + start_date=start_date, + end_date=end_date, + frequency=frequency, + ) + + # If no dates to download, skip processing + if not labeled_dates: + print(f"No new {name} image(s) to download for aez {aez}") + return + + download_jobs = [] + target_projection = None + if dataset == "modis_pet" and frequency == "monthly": + # Use CHIRPS monthly precipitation projection as the output grid for PET. + # This keeps local PET files aligned with CHIRPS and reproduces the + # original GEE reduceResolution(mean)->reproject(P.projection()) step. + first_date = labeled_dates[0][0] + first_chirps, _ = make_chirps_image( + ee.ImageCollection(CHIRPS_COLLECTION).select("precipitation"), + region, + first_date, + frequency, + ) + target_projection = first_chirps.projection() + + for index, (current_date, label) in enumerate(labeled_dates, start=1): + # Keeping one time step per request avoids the EE direct-download size + # error seen when trying to download a large multiband image directly. + band_name = f"{name}_{label}" + single_path = dataset_output_dir / f"{band_name}.tif" + + if single_path.exists() and not overwrite: + print(f"[{index}/{len(labeled_dates)}] exists: {single_path.name}") + else: + download_jobs.append((index, current_date, band_name, single_path)) + + if max_workers <= 1: + for index, current_date, band_name, single_path in download_jobs: + if dataset == "chirps": + image, _ = make_chirps_image( + collection, region, current_date, frequency + ) + else: + image, _ = make_modis_pet_image( + collection, region, current_date, frequency, target_projection + ) + print(f"[{index}/{len(labeled_dates)}] downloading: {single_path.name}") + download_image(image.rename(band_name), region, single_path, scale, crs) + time.sleep(sleep) + else: + # Parallel downloads are the main speed-up. Keep max_workers modest + # because Earth Engine may throttle too many simultaneous URL requests. + def download_one(job: tuple[int, ee.Date, str, Path]) -> tuple[int, str]: + index, current_date, band_name, single_path = job + if dataset == "chirps": + image, _ = make_chirps_image( + collection, region, current_date, frequency + ) + else: + image, _ = make_modis_pet_image( + collection, region, current_date, frequency, target_projection + ) + download_image(image.rename(band_name), region, single_path, scale, crs) + if sleep: + time.sleep(sleep) + return index, single_path.name + + print(f"Downloading {len(download_jobs)} file(s) with {max_workers} workers") + with ThreadPoolExecutor(max_workers=max_workers) as executor: + futures = [executor.submit(download_one, job) for job in download_jobs] + for future in as_completed(futures): + index, file_name = future.result() + print(f"[{index}/{len(labeled_dates)}] downloaded: {file_name}") + + print(f"Downloaded {len(labeled_dates)} {name} image(s) to {dataset_output_dir}") + + +def download_data_locally( + aez: int, + datasets: list[str] | str | None = None, + start_year: str = None, + end_year: str = None, + frequency: str = "monthly", + output_dir: str = "data/base_layers/spei/inputs", + sleep: float = 0.2, + max_workers: int = 4, + overwrite: bool = False, +) -> None: + # Public entry point for notebooks/scripts. datasets=["both"] downloads + # separate CHIRPS and MODIS PET time-step GeoTIFFs. + selected_datasets = datasets or ["both"] + # validate_inputs(aez, selected_datasets, frequency) + + start_date = f"{str(start_year)}-07-01" + end_date = f"{str(end_year+1)}-06-30" + + expanded_datasets = expand_datasets(selected_datasets) + for dataset in expanded_datasets: + scale = 5500 + crs = "EPSG:4326" + download_dataset_images( + aez=aez, + dataset=dataset, + start_date=start_date, + end_date=end_date, + frequency=frequency, + output_dir=output_dir, + scale=scale, + crs=crs, + sleep=sleep, + overwrite=overwrite, + max_workers=max_workers, + ) + + print("Done downloading data.") diff --git a/computing/spei/generate_spei/generate_ppet_multiband.py b/computing/spei/generate_spei/generate_ppet_multiband.py new file mode 100644 index 00000000..c523e354 --- /dev/null +++ b/computing/spei/generate_spei/generate_ppet_multiband.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import rasterio +from rasterio.enums import Resampling +from rasterio.warp import reproject + + +def find_monthly_file(dataset_dir: Path, prefix: str, year: int, month: int) -> Path: + """Find a downloaded monthly GeoTIFF, allowing nested folders from old runs.""" + label = f"{year}{month:02d}" + matches = sorted(dataset_dir.rglob(f"{prefix}_{label}.tif")) + if not matches: + raise FileNotFoundError( + f"Missing {prefix}_{label}.tif under {dataset_dir}. " + "Run download_chirps_local.py first." + ) + return matches[0] + + +def read_masked_band(dataset: rasterio.DatasetReader) -> np.ma.MaskedArray: + """Read band 1 and mask nodata plus any nan/inf values.""" + data = dataset.read(1, masked=True).astype("float32") + data = np.ma.masked_invalid(data) + return np.ma.masked_where(np.abs(data) > 1.0e20, data) + + +def reproject_modis_to_chirps_grid( + modis_path: Path, + chirps_dataset: rasterio.DatasetReader, + log_metadata: bool = False, +) -> np.ma.MaskedArray: + OUTPUT_NODATA = -9999.0 + """Reproject/resample MODIS PET onto the CHIRPS projection and pixel grid.""" + with rasterio.open(modis_path) as modis_src: + if log_metadata: + print( + " MODIS -> CHIRPS grid:", + f"modis_crs={modis_src.crs}", + f"chirps_crs={chirps_dataset.crs}", + f"modis_shape=({modis_src.height}, {modis_src.width})", + f"chirps_shape=({chirps_dataset.height}, {chirps_dataset.width})", + ) + + if ( + modis_src.crs == chirps_dataset.crs + and modis_src.transform == chirps_dataset.transform + and modis_src.width == chirps_dataset.width + and modis_src.height == chirps_dataset.height + ): + return read_masked_band(modis_src) + + pet_source = read_masked_band(modis_src) + pet_on_chirps_grid = np.full( + (chirps_dataset.height, chirps_dataset.width), + OUTPUT_NODATA, + dtype="float32", + ) + + # MODIS PET is finer (~500m) than CHIRPS (~5500m), so average is the + # right downsampling behavior for a monthly total/continuous variable. + reproject( + source=pet_source.filled(OUTPUT_NODATA), + destination=pet_on_chirps_grid, + src_transform=modis_src.transform, + src_crs=modis_src.crs, + dst_transform=chirps_dataset.transform, + dst_crs=chirps_dataset.crs, + dst_width=chirps_dataset.width, + dst_height=chirps_dataset.height, + src_nodata=OUTPUT_NODATA, + dst_nodata=OUTPUT_NODATA, + init_dest_nodata=True, + resampling=Resampling.average, + ) + return np.ma.masked_where( + (pet_on_chirps_grid == OUTPUT_NODATA) | ~np.isfinite(pet_on_chirps_grid), + pet_on_chirps_grid, + ) + + +def ppet_multiband( + aez=None, + start: int = 2004, + end: int = 2024, +) -> Path: + """ + SPEI Pipeline - Step 1 (Local P-PET) + + Uses GeoTIFFs downloaded by download_chirps_local.py instead of reading: + ee.ImageCollection("UCSB-CHG/CHIRPS/DAILY") + ee.ImageCollection("MODIS/061/MOD16A2GF").select("PET") + + Output: one local multiband GeoTIFF with one P-PET band per month. + Band names follow the original script: y{year}_m{month}, e.g. y2015_m06. + + """ + data_root = Path("data/base_layers/spei/inputs") + chirps_dir = data_root / str(aez) / "monthly" / "chirps" + modis_dir = data_root / str(aez) / "monthly" / "modis_pet" + + # input_root = Path("data/drought_inputs") + output_dir = data_root / str(aez) / "monthly" + output = output_dir / f"P_PET_AEZ_{str(aez)}_monthly_multiband.tif" + OUTPUT_NODATA = -9999.0 + + output_file = ( + Path(output) + if output + else data_root / str(aez) / f"P_PET_{str(aez)}_monthly_multiband.tif" + ) + output_file.parent.mkdir(parents=True, exist_ok=True) + + agri_months = list(range(7, 13)) + list(range(1, 7)) + months = [ + (year if month >= 7 else year + 1, month) + for year in range(start, end + 1) + for month in agri_months + ] + + # months = [(year, month) for year in range(start, end + 1) for month in range(1, 13)] + first_chirps = find_monthly_file(chirps_dir, "CHIRPS", *months[0]) + + with rasterio.open(first_chirps) as template: + profile = template.profile.copy() + profile.update( + count=len(months), + dtype="float32", + nodata=OUTPUT_NODATA, + compress="lzw", + BIGTIFF="IF_SAFER", + ) + + with rasterio.open(output_file, "w", **profile) as dst: + for band_index, (year, month) in enumerate(months, start=1): + chirps_path = find_monthly_file(chirps_dir, "CHIRPS", year, month) + modis_path = find_monthly_file(modis_dir, "MODIS_PET", year, month) + + with rasterio.open(chirps_path) as chirps_src: + precipitation = read_masked_band(chirps_src) + + # MODIS PET downloaded by download_chirps_local.py already has + # the 0.1 scale factor applied, so do not multiply again here. + pet = reproject_modis_to_chirps_grid( + modis_path, chirps_src, log_metadata=(band_index == 1) + ) + + precipitation_data = precipitation.filled(np.nan).astype("float32") + pet_data = pet.filled(np.nan).astype("float32") + + valid_mask = np.isfinite(precipitation_data) & np.isfinite(pet_data) + ppet_data = np.full( + precipitation_data.shape, OUTPUT_NODATA, dtype="float32" + ) + with np.errstate(invalid="ignore", over="ignore"): + ppet_data[valid_mask] = ( + precipitation_data[valid_mask] - pet_data[valid_mask] + ) + + ppet = np.ma.masked_where( + (ppet_data == OUTPUT_NODATA) | ~np.isfinite(ppet_data), ppet_data + ) + band_name = f"y{year}_m{month:02d}" + dst.write(ppet.filled(OUTPUT_NODATA).astype("float32"), band_index) + dst.set_band_description(band_index, band_name) + invalid_count = int(np.ma.count_masked(ppet)) + print(f"Prepared: {band_name} nodata_pixels={invalid_count}") + + print(f"\nWrote {len(months)} P-PET band(s): {output_file}") + return output_file diff --git a/computing/spei/generate_spei/merge_spei.py b/computing/spei/generate_spei/merge_spei.py new file mode 100644 index 00000000..e63bfda1 --- /dev/null +++ b/computing/spei/generate_spei/merge_spei.py @@ -0,0 +1,67 @@ +import glob +import rasterio +from rasterio.merge import merge + + +def merge_SPEI_rasters(spei_type): + # Input rasters + input_dir = f"data/base_layers/spei/outputs/SPEI_{spei_type}" + input_files = sorted(glob.glob(f"{input_dir}/SPEI{spei_type}_*.tif")) + + if not input_files: + raise RuntimeError("No input files found.") + + srcs = [rasterio.open(f) for f in input_files] + + # Validate all rasters have same number of bands + band_count = srcs[0].count + for src in srcs: + if src.count != band_count: + raise RuntimeError( + f"{src.name} has {src.count} bands, expected {band_count}" + ) + + # Output metadata + meta = srcs[0].meta.copy() + + # Merge first band to determine output size/transform + first_band, out_transform = merge(srcs, indexes=1, nodata=-9999, method="first") + + meta.update( + driver="GTiff", + height=first_band.shape[1], + width=first_band.shape[2], + transform=out_transform, + count=band_count, + compress="LZW", + tiled=True, + BIGTIFF="YES", + nodata=-9999, + ) + + output_file = f"{input_dir}/SPEI{spei_type}.tif" + + with rasterio.open(output_file, "w", **meta) as dst: + + # Copy band descriptions + dst.descriptions = srcs[0].descriptions + + for band in range(1, band_count + 1): + print(f"Merging band {band}/{band_count}") + + mosaic, _ = merge(srcs, indexes=[band], nodata=-9999, method="first") + + dst.write(mosaic[0], band) + + # Close inputs + for src in srcs: + src.close() + + print(f"Finished: {output_file}") + + +""" + After uploading the merged SPEI files on GEE, the band names gets renamed to b1, b2,..,etc. + To rename them back to relevant names, use the below GEE script: + https://code.earthengine.google.co.in/370dfca36f228fbd09892708949cf381 +""" diff --git a/computing/spei/generate_spei/spei_runner.py b/computing/spei/generate_spei/spei_runner.py new file mode 100644 index 00000000..39308606 --- /dev/null +++ b/computing/spei/generate_spei/spei_runner.py @@ -0,0 +1,31 @@ +from pathlib import Path +import subprocess + + +BASE_DIR = Path(__file__).resolve().parent.parent + +R_SCRIPT = BASE_DIR / "generate_spei" / "compute_spei.R" + + +def run_spei(aez=None, start_year=None, end_year=None): + print(aez) + command = ["Rscript", str(R_SCRIPT), str(aez), str(start_year), str(end_year)] + + print("COMMAND:", command) + + result = subprocess.run( + command, + capture_output=True, + text=True, + ) + + print("RETURN CODE:", result.returncode) + print("STDOUT:\n", result.stdout) + print("STDERR:\n", result.stderr) + + if result.returncode != 0: + raise Exception( + f" R Script Failed COMMAND:{command} RETURN CODE:{result.returncode} STDOUT:{result.stdout} STDERR:{result.stderr}" + ) + + return result.stdout diff --git a/computing/spei/high_wind_sensitivity/export_max_wind_index.py b/computing/spei/high_wind_sensitivity/export_max_wind_index.py new file mode 100644 index 00000000..f4a72ba0 --- /dev/null +++ b/computing/spei/high_wind_sensitivity/export_max_wind_index.py @@ -0,0 +1,145 @@ +import ee + +from utilities.constants import AEZ +from utilities.gee_utils import ( + ee_initialize, + is_gee_asset_exists, + export_raster_asset_to_gee, +) + + +def max_wind_index(aez, start_year=2004, end_year=2022, gee_account_id=None): + """ + * Forest Sensitivity Analysis Pipeline — Script 5a + * High Windspeed Index Export (Annual Max Hourly Windspeed, Hours > Threshold, Mean > Threshold) + * + * Computes three quantities per pixel per year and exports as a single + * multiband asset — three bands per year: + * + * WSmax_{year} = maximum hourly windspeed within the year (ERA5-Land) + * WShoursGT_{year} = total hours where windspeed > WIND_THRESHOLD + * WSmeanGT_{year} = mean windspeed during the hours it exceeded WIND_THRESHOLD + * + * Windspeed computed from ERA5-Land hourly u/v 10m wind components: + * windspeed = sqrt(u_component_of_wind_10m^2 + v_component_of_wind_10m^2) + * + * Output asset bands (e.g., for 2004-2022 = 19 years * 3 = 57 bands): + * WSmax_2004, WShoursGT_2004, WSmeanGT_2004, ... + * + * Requires: nothing — only public datasets (ERA5-Land Hourly) + """ + + ee_initialize(gee_account_id) + OUTPUT_DESC = f"wind_index_AEZ_{aez}" + OUTPUT_ASSET_ID = ( + f"projects/corestack-datasets-alpha/assets/datasets/SPEI/{OUTPUT_DESC}" + ) + + if is_gee_asset_exists(OUTPUT_ASSET_ID): + return None + + aoi = ee.FeatureCollection(AEZ).filter(ee.Filter.eq("ae_regcode", aez)).geometry() + # aoi = ( + # ee.FeatureCollection("projects/ext-datasets/assets/datasets/State_pan_india") + # .filter(ee.Filter.eq("Name", "Odisha")) + # .geometry() + # ) + + # Set your wind speed threshold here (in m/s) + WIND_THRESHOLD = 10.0 + + # =========================================================================== + # 1. HOURLY WINDSPEED FROM U/V COMPONENTS + # =========================================================================== + + era5Hourly = ( + ee.ImageCollection("ECMWF/ERA5_LAND/HOURLY") + .filterBounds(aoi) + .filterDate("2000-01-01", ee.Date.fromYMD(end_year, 12, 31)) + .select(["u_component_of_wind_10m", "v_component_of_wind_10m"]) + ) + + proj = era5Hourly.first().projection() + + def toWindSpeed(img): + ws = ( + img.select("u_component_of_wind_10m") + .pow(2) + .add(img.select("v_component_of_wind_10m").pow(2)) + .sqrt() + .rename("windspeed") + ) + return ws.copyProperties(img, ["system:time_start"]) + + windSpeedCol = era5Hourly.map(toWindSpeed) + + # =========================================================================== + # 2. ANNUAL METRICS (Max, Hours > Thresh, Mean > Thresh) + # =========================================================================== + + years = ee.List.sequence(start_year, end_year) + + def annual_metrics(y): + start = ee.Date.fromYMD(y, 1, 1) + # Note: filterDate is exclusive on the end date. Using y+1 ensures Dec 31 is included. + end = ee.Date.fromYMD(ee.Number(y).add(1), 1, 1) + + yearCol = windSpeedCol.filterDate(start, end) + + # 1. Max Wind Speed + wsMax = yearCol.max().rename("WSmax") + + # 2. Number of hours wind speed > threshold + wsHoursGT = yearCol.map( + lambda img: img.gt(WIND_THRESHOLD).rename("WShoursGT") + ).sum() + + # 3. Mean wind speed when > threshold + wsMeanGT = ( + yearCol.map(lambda img: img.updateMask(img.gt(WIND_THRESHOLD))) + .mean() + .rename("WSmeanGT") + ) + + # Combine all three into a single image for the year + return ( + ee.Image([wsMax, wsHoursGT, wsMeanGT]) + .setDefaultProjection(proj) + .set("year", y) + ) + + annual_ws_metrics = ee.ImageCollection(years.map(annual_metrics)) + + # =========================================================================== + # 3. STACK INTO SINGLE MULTIBAND IMAGE + # =========================================================================== + def add_year_bands(year, image): + year = ee.Number(year) + + year_img = annual_ws_metrics.filter(ee.Filter.eq("year", year)).first() + + year_str = ee.String(ee.Number(year).toInt()) + + ws_max = year_img.select("WSmax").rename(ee.String("WSmax_").cat(year_str)) + + ws_hours = year_img.select("WShoursGT").rename( + ee.String("WShoursGT_").cat(year_str) + ) + + ws_mean = year_img.select("WSmeanGT").rename( + ee.String("WSmeanGT_").cat(year_str) + ) + + return ee.Image(image).addBands(ws_max).addBands(ws_hours).addBands(ws_mean) + + empty_image = ee.Image().mask(ee.Image(0)) + output_image = ee.Image(years.iterate(add_year_bands, empty_image)) + + output_image = output_image.select(output_image.bandNames().remove("constant")) + + # Export execution block + task_id = export_raster_asset_to_gee( + output_image.clip(aoi), OUTPUT_DESC, OUTPUT_ASSET_ID, scale=11132, region=aoi + ) + + return task_id diff --git a/computing/spei/high_wind_sensitivity/highwind_resistance_resilience.py b/computing/spei/high_wind_sensitivity/highwind_resistance_resilience.py new file mode 100644 index 00000000..0fa5b5f0 --- /dev/null +++ b/computing/spei/high_wind_sensitivity/highwind_resistance_resilience.py @@ -0,0 +1,260 @@ +import ee + +from utilities.constants import AEZ +from utilities.gee_utils import ( + is_gee_asset_exists, + export_raster_asset_to_gee, + ee_initialize, +) + + +def high_wind_sensitivity(aez, start_year=2004, end_year=None, gee_account_id=None): + """ + * Forest Sensitivity Analysis Pipeline — Script 5b + * High Windspeed Resistance & Resilience (Harmonized kNDVI + Signed Formulas) + * + * For each forest pixel, computes mean resistance and resilience + * across all high-windspeed years (WSmax > threshold). + * + * Harmonization: Transforms Landsat 8/9 (OLI) to Landsat 5/7 (ETM+) + * equivalent before computing kNDVI to eliminate sensor-shift bias. + * + * Signed resistance (both +ve and -ve events): + * Resistance = Yn_bar / |Ye - Yn_bar| × sign(Ye - Yn_bar) + * + * Resilience computed ONLY when Ye < Yn_bar (negative effect years): + * Resilience = |Ye - Yn_bar| / |Ye+1 - Yn_bar| × sign(Ye+1 - Yn_bar) + * + * Requires: + * - Forest mask asset from Script 1 + * - Windspeed index asset from Script 5a + """ + ee_initialize(gee_account_id) + + TREE_COVER_ASSET = f"projects/corestack-datasets-alpha/assets/datasets/SPEI/Hybrid_Tree_AEZ_{aez}_{str(2003)}_{str(end_year)}" + WIND_INDEX_ASSET = ( + f"projects/corestack-datasets-alpha/assets/datasets/SPEI/wind_index_AEZ_{aez}" + ) + + OUTPUT_DESC = f"wind_metrics_harmonized_kNDVI_AEZ_{aez}" + OUTPUT_ASSET_ID = ( + f"projects/corestack-datasets-alpha/assets/datasets/SPEI/{OUTPUT_DESC}" + ) + + if is_gee_asset_exists(OUTPUT_ASSET_ID): + return None + + WIND_THRESHOLD = 15 + + BASELINE_START_YEAR = 2004 + BASELINE_END_YEAR = 2024 + + aoi = ee.FeatureCollection(AEZ).filter(ee.Filter.eq("ae_regcode", aez)).geometry() + # aoi = ( + # ee.FeatureCollection("projects/ext-datasets/assets/datasets/State_pan_india") + # .filter(ee.Filter.eq("Name", "Odisha")) + # .geometry() + # ) + # Loading the assets := + + treeMeta = ee.Image(TREE_COVER_ASSET) + startYear = treeMeta.select("start_year") + endYear = treeMeta.select("end_year") + + # Load windspeed index from single multiband asset (Script 5a output) + windIndex_raw = ee.Image(WIND_INDEX_ASSET) + + # I need WSmax bands covering BOTH my analysis window and my baseline + # window — whichever stretches further. Right now they're the same range + # (2004-2022), so this doesn't change anything today, but it protects me + # once the two windows stop lining up in the future. + wsMinYear = min(start_year, BASELINE_START_YEAR) + wsMaxYear = max(end_year, BASELINE_END_YEAR) + + wsImages = [] + for y in range(wsMinYear, wsMaxYear + 1): + wsImages.append( + windIndex_raw.select(f"WSmax_{y}").rename("windspeed").set("year", y) + ) + wsCol = ee.ImageCollection(wsImages) + + # LANDSAT HARMONIZATION & kNDVI := + + # Chastain et al. coefficients (OLI to ETM+) + chastainBandNames = ["BLUE", "GREEN", "RED", "NIR", "SWIR1", "SWIR2"] + oliETMSlopes = ee.Image.constant( + [1.03501, 1.00921, 1.01991, 1.14061, 1.04351, 1.05271] + ) + oliETMIntercepts = ee.Image.constant( + [-0.0055, -0.0008, -0.0021, -0.0163, -0.0045, 0.00261] + ) + + # Pre-process Landsat 5/7 (Baseline) + def prepL57(image): + qa = image.select("QA_PIXEL") + mask = qa.bitwiseAnd(1 << 3).eq(0).And(qa.bitwiseAnd(1 << 4).eq(0)) + + # Apply mask, select optical bands, and apply Collection 2 scale factors + scaled = ( + image.updateMask(mask) + .select(["SR_B1", "SR_B2", "SR_B3", "SR_B4", "SR_B5", "SR_B7"]) + .multiply(0.0000275) + .add(-0.2) + ) + + return scaled.rename(chastainBandNames).copyProperties( + image, ["system:time_start"] + ) + + # Pre-process Landsat 8/9 and Harmonize to ETM+ + def prepL89(image): + qa = image.select("QA_PIXEL") + mask = qa.bitwiseAnd(1 << 3).eq(0).And(qa.bitwiseAnd(1 << 4).eq(0)) + + # Apply mask, select optical bands, and apply Collection 2 scale factors + scaled = ( + image.updateMask(mask) + .select(["SR_B2", "SR_B3", "SR_B4", "SR_B5", "SR_B6", "SR_B7"]) + .multiply(0.0000275) + .add(-0.2) + .rename(chastainBandNames) + ) + + # Apply Chastain regression model (OLI -> ETM+) + harmonized = scaled.multiply(oliETMSlopes).add(oliETMIntercepts) + + return harmonized.copyProperties(image, ["system:time_start"]) + + # Calculate annual median kNDVI + def getAnnualKNDVI(year): + start = ee.Date.fromYMD(year, 1, 1) + end = ee.Date.fromYMD(year, 12, 31) + + l89 = ( + ee.ImageCollection("LANDSAT/LC08/C02/T1_L2") + .merge(ee.ImageCollection("LANDSAT/LC09/C02/T1_L2")) + .filterDate(start, end) + .filterBounds(aoi) + .map(prepL89) + .map( + lambda img: img.normalizedDifference(["NIR", "RED"]) + .pow(2) + .tanh() + .rename("kndvi") + ) + ) + + l57 = ( + ee.ImageCollection("LANDSAT/LT05/C02/T1_L2") + .merge(ee.ImageCollection("LANDSAT/LE07/C02/T1_L2")) + .filterDate(start, end) + .filterBounds(aoi) + .map(prepL57) + .map( + lambda img: img.normalizedDifference(["NIR", "RED"]) + .pow(2) + .tanh() + .rename("kndvi") + ) + ) + + return l89.merge(l57).median().set("year", year).rename("kndvi") + + # Load kNDVI for START_YEAR to END_YEAR+1 (need next year for resilience) + # I need kNDVI images covering whichever is bigger: my baseline window, or + # my analysis window PLUS ONE year (since resilience for my last analysis + # year needs next-year kNDVI to compare against). + + kndviMinYear = min(start_year, BASELINE_START_YEAR) + kndviMaxYear = max(end_year + 1, BASELINE_END_YEAR) + + kndviYears = ee.List.sequence(kndviMinYear, kndviMaxYear) + kndviCol = ee.ImageCollection(kndviYears.map(getAnnualKNDVI)) + + # BASELINE kNDVI (Yn_bar):= + # Mean kNDVI across non-high-wind years only + + analysisYears = ee.List.sequence(start_year, end_year) + + # This is my frozen baseline window — the years I use to WORK OUT what + # Yn_bar (my "normal" kNDVI reference) is, kept separate from + # analysisYears on purpose. + baselineYears = ee.List.sequence(BASELINE_START_YEAR, BASELINE_END_YEAR) + + def calc_kndvi_non_event(y): + year = ee.Number(y) + kndvi = kndviCol.filter(ee.Filter.eq("year", year)).first() + ws = ( + wsCol.filter(ee.Filter.eq("year", year)) + .first() + .resample("bilinear") + .reproject(crs=kndvi.projection(), scale=30) + ) + isNonEvent = ws.lte(WIND_THRESHOLD) + return kndvi.updateMask(isNonEvent).set("year", year) + + kndviNonEvent = ee.ImageCollection(baselineYears.map(calc_kndvi_non_event)) + + Yn_bar = kndviNonEvent.mean().rename("kndvi_baseline") + + # SIGNED RESISTANCE & RESILIENCE := + def calc_metrics_col(y): + year = ee.Number(y) + + kndviYe = kndviCol.filter(ee.Filter.eq("year", year)).first() + wsYe = ( + wsCol.filter(ee.Filter.eq("year", year)) + .first() + .resample("bilinear") + .reproject(crs=kndviYe.projection(), scale=30) + ) + + # Only compute on forest pixels during high-windspeed years + # Flag ANY year where WSmax crosses the threshold, however briefly + isForest = startYear.lte(year).And(endYear.gte(year)) + isHighWind = wsYe.gt(WIND_THRESHOLD) + eventMask = isForest.And(isHighWind) + + diffRaw = kndviYe.subtract(Yn_bar) + diffAbs = diffRaw.abs().max(1e-6) + + # Resistance: signed, computed for ALL high-wind years + resistance = ( + Yn_bar.divide(diffAbs) + .multiply(diffRaw.signum()) + .rename("resistance") + .updateMask(eventMask) + ) + + # Resilience: ONLY computed when Ye < Yn_bar (negative effect years) + isNegativeEffect = kndviYe.lt(Yn_bar) + resilMask = eventMask.And(isNegativeEffect) + + kndviNext = kndviCol.filter(ee.Filter.eq("year", year.add(1))).first() + diffNext = kndviNext.subtract(Yn_bar) + diffNextAbs = diffNext.abs().max(1e-6) + + resilience = ( + diffAbs.divide(diffNextAbs) + .multiply(diffNext.signum()) + .rename("resilience") + .updateMask(resilMask) + ) + + return ee.Image.cat([resistance, resilience]).set("year", year) + + metricsCol = ee.ImageCollection(analysisYears.map(calc_metrics_col)) + + # AGGREGATE & EXPORT + meanResistance = metricsCol.select("resistance").mean().clip(aoi) + meanResilience = metricsCol.select("resilience").mean().clip(aoi) + + finalOutput = meanResistance.rename("resistance").addBands( + meanResilience.rename("resilience") + ) + + task_id = export_raster_asset_to_gee( + finalOutput, OUTPUT_DESC, OUTPUT_ASSET_ID, scale=30, region=aoi + ) + + return task_id diff --git a/computing/spei/hybrid_tree_mask.py b/computing/spei/hybrid_tree_mask.py new file mode 100644 index 00000000..8e0520a5 --- /dev/null +++ b/computing/spei/hybrid_tree_mask.py @@ -0,0 +1,185 @@ +import ee +from utilities.constants import AEZ +from utilities.gee_utils import ( + export_raster_asset_to_gee, + ee_initialize, + is_gee_asset_exists, +) + + +def generate_hybrid_tree_mask(aez, start_year=2004, end_year=None, gee_account_id=None): + """ + Forest Sensitivity Analysis Pipeline — Script 1 + Hybrid 30m Annual Tree Cover Mask + Contiguous Forest Period + + Produces per-pixel: length (years), start_year, end_year + of the most recent unbroken forest period (2003–2022). + + Sources: + 1. GLC-FCS30D (2003–2022) + 2. . Dynamic World (2015–present) + 3. IndiaSat LULC (2017–present), Core-stack. + + Union logic: majority vote among active datasets per year. + GLC-FCS30D: 2003–2022 (classes 51–92) + Dynamic World: 2015–present (class 1 = Trees) + IndiaSat LULC: 2017–present (class 6 = Trees) + + Temporal correction: ±2 year window as used in other places too by the team. + """ + + ee_initialize(gee_account_id) + + TEMPORAL_WINDOW = 2 + + start_year = 2004 + LULC_START_YEAR = 2017 + + OUTPUT_DESC = f"Hybrid_Tree_AEZ_{aez}_{str(start_year)}_{str(end_year)}" + OUTPUT_ASSET_ID = ( + f"projects/corestack-datasets-alpha/assets/datasets/SPEI/{OUTPUT_DESC}" + ) + + if is_gee_asset_exists(OUTPUT_ASSET_ID): + return None, OUTPUT_ASSET_ID + + aoi = ee.FeatureCollection(AEZ).filter(ee.Filter.eq("ae_regcode", aez)).geometry() + # aoi = ( + # ee.FeatureCollection("projects/ext-datasets/assets/datasets/State_pan_india") + # .filter(ee.Filter.eq("Name", "Odisha")) + # .geometry() + # ) + + # DATASET PREPARATION := + # --- GLC-FCS30D --- + glcMosaic = ee.ImageCollection( + "projects/sat-io/open-datasets/GLC-FCS30D/annual" + ).mosaic() + + # --- Dynamic World --- + dwCol = ee.ImageCollection("GOOGLE/DYNAMICWORLD/V1").filterBounds(aoi) + + # --- IndiaSat LULC --- + indiaSatList = [] + for year in range(LULC_START_YEAR, end_year + 1): + indiaSatList.append( + ee.Image( + f"projects/corestack-datasets/assets/datasets/LULC_v3_river_basin/pan_india_lulc_v3_{year}_{year+1}" + ).set("year", year) + ) + + indiaSatCol = ee.ImageCollection(indiaSatList) + + def get_indiaSat_mask(y): + img = indiaSatCol.filter(ee.Filter.eq("year", y)).first() + return ee.Image( + ee.Algorithms.If( + img, + ee.Image(img).select("predicted_label").eq(6).unmask(0), + ee.Image(0), + ) + ).rename("tree") + + # HYBRID MASK GENERATION := + years = ee.List.sequence(start_year, end_year) + + def annual_tree_cover(year): + year = ee.Number(year) + + # GLC — always active, classes 51–92 are forests + bandName = ee.String("b").cat(year.subtract(1999).format("%.0f")) + + glcMask = ee.Image( + ee.Algorithms.If( + year.lte(2022), + glcMosaic.select(bandName) + .gte(51) + .And(glcMosaic.select(bandName).lte(92)), + ee.Image(0), + ) + ).rename("tree") + proj = glcMask.projection() + + # Dynamic World — active from 2015 + dwYear = dwCol.filter(ee.Filter.calendarRange(year, year, "year")).select( + "label" + ) + dwMask = ( + ee.Image( + ee.Algorithms.If( + dwYear.size().gt(0), dwYear.mode().eq(1).unmask(0), ee.Image(0) + ) + ) + .rename("tree") + .reproject(crs=proj, scale=30) + ) + + # IndiaSat — active from 2017 + indiaSatMask = get_indiaSat_mask(year).reproject(crs=proj, scale=30) + + # Majority vote + glcActive = ee.Algorithms.If(year.lte(2022), 1, 0) + forestSum = glcMask.unmask(0).add(dwMask.unmask(0)).add(indiaSatMask.unmask(0)) + dwActive = ee.Algorithms.If(year.gte(2015), 1, 0) + indiasatActive = ee.Algorithms.If(year.gte(2017).And(year.lte(2024)), 1, 0) + activeDatasets = ee.Number(glcActive).add(dwActive).add(indiasatActive) + requiredVotes = ee.Algorithms.If(activeDatasets.eq(1), 1, 2) + hybridMask = forestSum.gte(ee.Number(requiredVotes)) + + return hybridMask.set("year", year).rename("tree") + + annualTreeCoverMasks = ee.ImageCollection(years.map(annual_tree_cover)) + + # TEMPORAL CORRECTION := + def corrected_tree_cover(year): + year = ee.Number(year) + originalMask = annualTreeCoverMasks.filter(ee.Filter.eq("year", year)).first() + + windowMasks = annualTreeCoverMasks.filter( + ee.Filter.And( + ee.Filter.neq("year", year), + ee.Filter.gte("year", year.subtract(TEMPORAL_WINDOW)), + ee.Filter.lte("year", year.add(TEMPORAL_WINDOW)), + ) + ) + + corrected = originalMask.unmask(0).where(windowMasks.max().eq(1), 1) + return corrected.set("year", year).rename("tree") + + correctedTreeCoverMasks = ee.ImageCollection(years.map(corrected_tree_cover)) + + # CONTIGUOUS FOREST PERIOD (LENGTH, START, END) := + def calculate_consecutive(currentImage, previousState): + prevCount = ee.Image(ee.List(previousState).get(0)) + prevStop = ee.Image(ee.List(previousState).get(1)) + currentMask = currentImage.select("tree") + stillCounting = prevStop.Not() + newCount = prevCount.add(currentMask.multiply(stillCounting)) + newStop = prevStop.Or(currentMask.Not()) + return ee.List([newCount, newStop]) + + # Iterate backwards so we get the MOST RECENT contiguous period + reversedCollection = correctedTreeCoverMasks.sort("year", False) + initialState = ee.List([ee.Image(0).byte(), ee.Image(0).byte()]) + finalState = ee.List( + reversedCollection.iterate(calculate_consecutive, initialState) + ) + + recentLength = ee.Image(finalState.get(0)).rename("length") + forestEndYear = ee.Image(end_year).multiply(recentLength.gt(0)).rename("end_year") + forestStartYear = ( + forestEndYear.subtract(recentLength) + .add(1) + .multiply(recentLength.gt(0)) + .rename("start_year") + ) + + finalOutput = ( + recentLength.addBands(forestStartYear).addBands(forestEndYear).clip(aoi) + ) + + task_id = export_raster_asset_to_gee( + finalOutput, OUTPUT_DESC, OUTPUT_ASSET_ID, scale=30, region=aoi + ) + + return task_id, OUTPUT_ASSET_ID diff --git a/computing/spei/rainfall_sensitivity/export_rainfall_index.py b/computing/spei/rainfall_sensitivity/export_rainfall_index.py new file mode 100644 index 00000000..57bf0946 --- /dev/null +++ b/computing/spei/rainfall_sensitivity/export_rainfall_index.py @@ -0,0 +1,180 @@ +import ee + +from utilities.constants import AEZ +from utilities.gee_utils import ( + ee_initialize, + is_gee_asset_exists, + export_raster_asset_to_gee, +) + + +def rainfall_index(aez, start_year=2004, end_year=None, gee_account_id=None): + """ + * Forest Sensitivity Analysis Pipeline — Script 3a + * Heavy Rainfall Index Export (Wet-Days Only Threshold + Extended Metrics) + * + * Computes 5 quantities per pixel per year and exports as a single + * multiband asset — one band per year for each quantity: + * + * Hm_{year} = annual sum of precipitation on heavy days + * zScore_{year} = z-score of Hm relative to the 2004-2022 period + * heavyDays_{year} = number of days in the year with heavy rainfall + * heavyAvg_{year} = average daily precipitation on heavy rainfall days + * maxDay_{year} = maximum daily precipitation recorded in that year + * + * Heavy day definition: daily precipitation > long-term 95th percentile + * of WET DAYS ONLY (> 1mm) (CHIRPS) + * + * This asset is the direct input to Script 3b, analogous to how + * SPEI assets are the input to Script 2 (drought). + * + * Requires: nothing — only public datasets (CHIRPS) + """ + + ee_initialize(gee_account_id) + OUTPUT_DESC = f"rain_index_AEZ_{aez}" + OUTPUT_ASSET_ID = ( + f"projects/corestack-datasets-alpha/assets/datasets/SPEI/{OUTPUT_DESC}" + ) + + if is_gee_asset_exists(OUTPUT_ASSET_ID): + return None + + # Fixed baseline window for zScore normalization — independent of START_YEAR/END_YEAR. + # Do NOT change this once results are published, or old zScore bands will drift + # when the pipeline timeline is extended. + BASELINE_START_YEAR = 2004 + BASELINE_END_YEAR = 2024 + + # 1. AOI + aoi = ee.FeatureCollection(AEZ).filter(ee.Filter.eq("ae_regcode", aez)).geometry() + # aoi = ( + # ee.FeatureCollection("projects/ext-datasets/assets/datasets/State_pan_india") + # .filter(ee.Filter.eq("Name", "Odisha")) + # .geometry() + # ) + + # 2. BASELINE HEAVY RAINFALL THRESHOLD + chirps = ( + ee.ImageCollection("UCSB-CHG/CHIRPS/DAILY") + .filterBounds(aoi) + .filterDate("2000-01-01", ee.Date.fromYMD(BASELINE_END_YEAR, 12, 31)) + .select("precipitation") + ) + + proj = chirps.first().projection() + + # Long-term 95th percentile of WET DAYS ONLY (> 1mm) + p95 = ( + chirps.map(lambda img: img.updateMask(img.gt(1))) + .reduce(ee.Reducer.percentile([95])) + .setDefaultProjection(proj) + .rename("p95") + ) + + # 3. ANNUAL METRICS CALCULATION + + metricsMinYear = min(start_year, BASELINE_START_YEAR) + metricsMaxYear = max(end_year, BASELINE_END_YEAR) + metricsYears = ee.List.sequence(metricsMinYear, metricsMaxYear) + + def calc_annual_metrics(y): + start = ee.Date.fromYMD(y, 1, 1) + end = ee.Date.fromYMD(y, 12, 31) + + yearCollection = chirps.filterDate(start, end) + + # Absolute maximum daily rainfall for this year + maxDay = ( + yearCollection.max().unmask(0).setDefaultProjection(proj).rename("maxDay") + ) + + # Binary mask: isolated heavy rain days + heavyRainCollection = yearCollection.map( + lambda img: img.updateMask(img.gt(p95)) + ) + + # 1. Annual sum of heavy rainfall + hm = ( + heavyRainCollection.map(lambda img: img.unmask(0)) + .sum() + .setDefaultProjection(proj) + .rename("Hm") + ) + + # 2. Number of heavy days + heavyDays = ( + heavyRainCollection.map(lambda img: img.mask()) + .sum() + .unmask(0) + .setDefaultProjection(proj) + .rename("heavyDays") + ) + + # 3. Average intensity of heavy days + heavyAvg = ( + heavyRainCollection.mean() + .unmask(0) + .setDefaultProjection(proj) + .rename("heavyAvg") + ) + + # Combine metrics into a single image per year containing all 4 base properties + return hm.addBands(heavyDays).addBands(heavyAvg).addBands(maxDay).set("year", y) + + annualMetrics = ee.ImageCollection(metricsYears.map(calc_annual_metrics)) + + # 4. FIXED Z-SCORE CALCULATION + + baselineYears = ee.List.sequence(BASELINE_START_YEAR, BASELINE_END_YEAR) + + baselineMetrics = annualMetrics.filter(ee.Filter.inList("year", baselineYears)) + + hmMean = baselineMetrics.select("Hm").mean() + hmStdDev = baselineMetrics.select("Hm").reduce(ee.Reducer.stdDev()) + + # Changed to server-side map architecture instead of the annual image collection, done for GEE optimisation.. as it's a better practice. + def calc_annual_metrics(img): + hm = img.select("Hm") + z = hm.subtract(hmMean).divide(hmStdDev).rename("zScore") + return img.addBands(z) + + completedAnnualCollection = annualMetrics.map(calc_annual_metrics) + + # 5. SERVER-SIDE STACK INTO SINGLE MULTIBAND IMAGE & EXPORT + analysisYears = ee.List.sequence(start_year, end_year) + + # Changed server-side iteration style to stack and correctly rename bands + def add_bands_for_year(y, acc): + yearStr = ee.String(ee.Number(y).toInt()) + yearImg = completedAnnualCollection.filter(ee.Filter.eq("year", y)).first() + + hmBand = yearImg.select("Hm").rename(ee.String("Hm_").cat(yearStr)) + zBand = yearImg.select("zScore").rename(ee.String("zScore_").cat(yearStr)) + heavyDaysBand = yearImg.select("heavyDays").rename( + ee.String("heavyDays_").cat(yearStr) + ) + heavyAvgBand = yearImg.select("heavyAvg").rename( + ee.String("heavyAvg_").cat(yearStr) + ) + maxDayBand = yearImg.select("maxDay").rename(ee.String("maxDay_").cat(yearStr)) + + return ( + ee.Image(acc) + .addBands(hmBand) + .addBands(zBand) + .addBands(heavyDaysBand) + .addBands(heavyAvgBand) + .addBands(maxDayBand) + ) + + empty_image = ee.Image().mask(ee.Image(0)) + output_image = ee.Image(analysisYears.iterate(add_bands_for_year, empty_image)) + + output_image = output_image.select(output_image.bandNames().remove("constant")) + + task_id = export_raster_asset_to_gee( + output_image.clip(aoi), OUTPUT_DESC, OUTPUT_ASSET_ID, scale=5566, region=aoi + ) + + return task_id diff --git a/computing/spei/rainfall_sensitivity/rainfall_resistance_resilience.py b/computing/spei/rainfall_sensitivity/rainfall_resistance_resilience.py new file mode 100644 index 00000000..29711a4a --- /dev/null +++ b/computing/spei/rainfall_sensitivity/rainfall_resistance_resilience.py @@ -0,0 +1,288 @@ +import ee + +from utilities.constants import AEZ +from utilities.gee_utils import ( + ee_initialize, + is_gee_asset_exists, + export_raster_asset_to_gee, +) + + +def generate_rainfall_resilience( + aez, start_year=2004, end_year=None, gee_account_id=None +): + """ + * Forest Sensitivity Analysis Pipeline — Script 3b + * Heavy Rainfall Resistance & Resilience (Harmonized kNDVI + Signed Formulas) + * + * Signed resistance (both +ve and -ve events): + * Resistance = Yn_bar / |Ye - Yn_bar| × sign(Ye - Yn_bar) + * + * Resilience computed ONLY when Ye < Yn_bar (negative effect years): + * Resilience = |Ye - Yn_bar| / |Ye+1 - Yn_bar| × sign(Ye+1 - Yn_bar) + * + * Harmonization: Transforms Landsat 8/9 (OLI) to Landsat 5/7 (ETM+) + * equivalent before computing kNDVI to eliminate sensor-shift bias. + * + * Requires: + * - Forest mask asset (Script 1) + * - Rainfall index asset (Script 3a) + """ + + ee_initialize(gee_account_id) + TREE_COVER_ASSET = f"projects/corestack-datasets-alpha/assets/datasets/SPEI/Hybrid_Tree_AEZ_{aez}_{str(2003)}_{str(end_year)}" + RAIN_INDEX_ASSET = ( + f"projects/corestack-datasets-alpha/assets/datasets/SPEI/rain_index_AEZ_{aez}" + ) + + OUTPUT_DESC = f"Rain_Metrics_{aez}" # f"Rain_Metrics_AEZ_{aez}" + OUTPUT_ASSET_ID = ( + f"projects/corestack-datasets-alpha/assets/datasets/SPEI/{OUTPUT_DESC}" + ) + + if is_gee_asset_exists(OUTPUT_ASSET_ID): + return None + + Z_THRESHOLD = 1.0 + + # This is the same fix I did for the drought script (Script 2). Yn_bar is + # my baseline — "what kNDVI should normally look like on a healthy, + # non-anomalous year." If I let Yn_bar be computed only from + # START_YEAR..END_YEAR, then every time I extend my analysis window in + # the future, Yn_bar shifts a little, and that quietly changes ALL my + # past resistance/resilience numbers too — even for years I already + # published. That's the exact problem I don't want. + # + # So I'm freezing the baseline window here, separately from my analysis + # window. Once I publish results, I'm never touching these two numbers + # again — if I do, my old outputs will drift. + # + # I picked 2004-2024 to match what I already locked in for the drought + # baseline (Script 2) and for the rain z-score baseline (Script 3a) — + # keeping all three consistent. 2004 is my floor because that's as far + # back as my source data goes; 2024 is what I've already confirmed is + # available and exported (3a's END_YEAR is now 2024, so the zScore bands + # I need actually exist in the rain index asset). + BASELINE_START_YEAR = 2004 + BASELINE_END_YEAR = 2024 + + aoi = ee.FeatureCollection(AEZ).filter(ee.Filter.eq("ae_regcode", aez)).geometry() + # aoi = ( + # ee.FeatureCollection("projects/ext-datasets/assets/datasets/State_pan_india") + # .filter(ee.Filter.eq("Name", "Odisha")) + # .geometry() + # ) + + treeMeta = ee.Image(TREE_COVER_ASSET) + startYearTree = treeMeta.select("start_year") + endYearTree = treeMeta.select("end_year") + + rainIndex = ee.Image(RAIN_INDEX_ASSET) + + # I need zScore bands for every year covering BOTH my baseline window + # and my analysis window — whichever stretches further in each + # direction. Before, this loop only went START_YEAR..END_YEAR, which + # meant if I ever widened my baseline beyond my analysis window, I'd be + # trying to .select() a band like zScore_2024 that this loop never even + # asked for. So I'm building the year range from the min/max of both + # windows, to be safe. + zMinYear = min(start_year, BASELINE_START_YEAR) + zMaxYear = max(end_year, BASELINE_END_YEAR) + + zScoreCol_list = [] + for y in range(zMinYear, zMaxYear + 1): + zScoreCol_list.append( + rainIndex.select("zScore_" + str(y)).rename("zScore").set("year", y) + ) + + zScoreCol = ee.ImageCollection(zScoreCol_list) + + # LANDSAT HARMONIZATION & kNDVI := + + chastainBandNames = ["BLUE", "GREEN", "RED", "NIR", "SWIR1", "SWIR2"] + oliETMSlopes = ee.Image.constant( + [1.03501, 1.00921, 1.01991, 1.14061, 1.04351, 1.05271] + ) + oliETMIntercepts = ee.Image.constant( + [-0.0055, -0.0008, -0.0021, -0.0163, -0.0045, 0.00261] + ) + + # Pre-process Landsat 5/7 (Baseline) + def prepL57(image): + qa = image.select("QA_PIXEL") + mask = qa.bitwiseAnd(1 << 3).eq(0).And(qa.bitwiseAnd(1 << 4).eq(0)) + + scaled = ( + image.updateMask(mask) + .select(["SR_B1", "SR_B2", "SR_B3", "SR_B4", "SR_B5", "SR_B7"]) + .multiply(0.0000275) + .add(-0.2) + ) + + return scaled.rename(chastainBandNames).copyProperties( + image, ["system:time_start"] + ) + + # Pre-process Landsat 8/9 and Harmonize to ETM+ + def prepL89(image): + qa = image.select("QA_PIXEL") + mask = qa.bitwiseAnd(1 << 3).eq(0).And(qa.bitwiseAnd(1 << 4).eq(0)) + + scaled = ( + image.updateMask(mask) + .select(["SR_B2", "SR_B3", "SR_B4", "SR_B5", "SR_B6", "SR_B7"]) + .multiply(0.0000275) + .add(-0.2) + .rename(chastainBandNames) + ) + + harmonized = scaled.multiply(oliETMSlopes).add(oliETMIntercepts) + + return harmonized.copyProperties(image, ["system:time_start"]) + + # Calculate annual median kNDVI + def getAnnualKNDVI(year): + start = ee.Date.fromYMD(year, 1, 1) + end = ee.Date.fromYMD(year, 12, 31) + + l89 = ( + ee.ImageCollection("LANDSAT/LC08/C02/T1_L2") + .merge(ee.ImageCollection("LANDSAT/LC09/C02/T1_L2")) + .filterDate(start, end) + .filterBounds(aoi) + .map(prepL89) + .map( + lambda img: img.normalizedDifference(["NIR", "RED"]) + .pow(2) + .tanh() + .rename("kndvi") + ) + ) + + l57 = ( + ee.ImageCollection("LANDSAT/LT05/C02/T1_L2") + .merge(ee.ImageCollection("LANDSAT/LE07/C02/T1_L2")) + .filterDate(start, end) + .filterBounds(aoi) + .map(prepL57) + .map( + lambda img: img.normalizedDifference(["NIR", "RED"]) + .pow(2) + .tanh() + .rename("kndvi") + ) + ) + + return l89.merge(l57).median().set("year", year).rename("kndvi") + + # Same idea as zScoreCol above — I need kNDVI images covering whichever + # is bigger: my baseline window, or my analysis window (+1 year, because + # resilience for my LAST analysis year needs next-year kNDVI to compare + # against). So I'm taking the min of the two start years and the max of + # (END_YEAR + 1) vs BASELINE_END_YEAR. + kndviMinYear = min(start_year, BASELINE_START_YEAR) + kndviMaxYear = max(end_year + 1, BASELINE_END_YEAR) + + kndviYears = ee.List.sequence(kndviMinYear, kndviMaxYear) + kndviCol = ee.ImageCollection(kndviYears.map(getAnnualKNDVI)) + + # BASELINE kNDVI (Yn_bar) := + # Mean kNDVI across non-anomalous years only + + # This is my actual analysis window — the years I want resistance and + # resilience results FOR. This stays exactly as before, untouched. + + analysisYears = ee.List.sequence(start_year, end_year) + + # This is my frozen baseline window — the years I use to WORK OUT what + # Yn_bar (my "normal" kNDVI reference) is. This is separate from + # analysisYears on purpose, so extending my analysis later doesn't shift + # Yn_bar and quietly rewrite results I've already published. + baselineYears = ee.List.sequence(BASELINE_START_YEAR, BASELINE_END_YEAR) + + def calc_Yn_bar(y): + year = ee.Number(y) + kndvi = ee.Image(kndviCol.filter(ee.Filter.eq("year", year)).first()) + zScore = ( + ee.Image(zScoreCol.filter(ee.Filter.eq("year", year)).first()) + .resample("bilinear") + .reproject(crs=kndvi.projection(), scale=30) + ) + + isNormal = zScore.select("zScore").abs().lt(Z_THRESHOLD) + isForest = startYearTree.lte(year).And(endYearTree.gte(year)) + + return kndvi.updateMask(isNormal.And(isForest)).set("year", year) + + Yn_bar = ( + ee.ImageCollection(baselineYears.map(calc_Yn_bar)) + .mean() + .rename("kndvi_baseline") + ) + + # SIGNED RESISTANCE & RESILIENCE := + + # This part is completely unchanged from before — it still only runs + # over analysisYears (my actual START_YEAR..END_YEAR), it just now uses + # the frozen Yn_bar computed above instead of a Yn_bar that would've + # silently moved every time I touch START_YEAR/END_YEAR. + + def calc_metrics_cols(y): + year = ee.Number(y) + + kndviYe = ee.Image(kndviCol.filter(ee.Filter.eq("year", year)).first()) + zScore = ( + ee.Image(zScoreCol.filter(ee.Filter.eq("year", year)).first()) + .resample("bilinear") + .reproject(crs=kndviYe.projection(), scale=30) + ) + + # Only compute on forest pixels during anomalous rainfall years + isAnomalous = zScore.select("zScore").gt(Z_THRESHOLD) + isForest = startYearTree.lte(year).And(endYearTree.gte(year)) + eventMask = isAnomalous.And(isForest) + + diffRaw = kndviYe.subtract(Yn_bar) + diffAbs = diffRaw.abs().max(1e-6) + + # Resistance: signed, computed for ALL anomalous years (both +ve and -ve) + resistance = ( + Yn_bar.divide(diffAbs) + .multiply(diffRaw.signum()) + .rename("resistance") + .updateMask(eventMask) + ) + + # Resilience: ONLY computed when Ye < Yn_bar (negative effect years) + isNegativeEffect = kndviYe.lt(Yn_bar) + resilMask = eventMask.And(isNegativeEffect) + + kndviNext = ee.Image(kndviCol.filter(ee.Filter.eq("year", year.add(1))).first()) + diffNext = kndviNext.subtract(Yn_bar) + diffNextAbs = diffNext.abs().max(1e-6) + + resilience = ( + diffAbs.divide(diffNextAbs) + .multiply(diffNext.signum()) + .rename("resilience") + .updateMask(resilMask) + ) + + return ee.Image.cat([resistance, resilience]).set("year", year) + + metricsCol = ee.ImageCollection(analysisYears.map(calc_metrics_cols)) + + # AGGREGATE & EXPORT := + + meanResist = metricsCol.select("resistance").mean().clip(aoi) + meanResil = metricsCol.select("resilience").mean().clip(aoi) + + finalOutput = meanResist.rename("resistance").addBands( + meanResil.rename("resilience") + ) + + task_id = export_raster_asset_to_gee( + finalOutput, OUTPUT_DESC, OUTPUT_ASSET_ID, scale=30, region=aoi + ) + + return task_id diff --git a/computing/spei/spei.py b/computing/spei/spei.py new file mode 100644 index 00000000..e1ab1bf6 --- /dev/null +++ b/computing/spei/spei.py @@ -0,0 +1,140 @@ +from computing.spei.drought_sensitivity.drought_resistance_resilience import ( + generate_drought_resistance, +) +from computing.spei.forestfire_sensitivity.export_fire_index import fire_index +from computing.spei.forestfire_sensitivity.forest_fire_resistance_resilience import ( + forest_fire_sensitivity, +) +from computing.spei.generate_spei.download_base_datasets import download_data_locally +from computing.spei.generate_spei.generate_ppet_multiband import ppet_multiband +from computing.spei.generate_spei.merge_spei import merge_SPEI_rasters +from computing.spei.generate_spei.spei_runner import run_spei +from computing.spei.high_wind_sensitivity.export_max_wind_index import max_wind_index +from computing.spei.high_wind_sensitivity.highwind_resistance_resilience import ( + high_wind_sensitivity, +) +from computing.spei.hybrid_tree_mask import generate_hybrid_tree_mask +from computing.spei.rainfall_sensitivity.export_rainfall_index import rainfall_index +from computing.spei.rainfall_sensitivity.rainfall_resistance_resilience import ( + generate_rainfall_resilience, +) +from utilities.gee_utils import ee_initialize, check_task_status, is_gee_asset_exists +from nrm_app.celery import app + + +@app.task(bind=True) +def generate_spei_pipeline( + self, + aez=None, + start_year=2004, + end_year=2024, + gee_account_id=None, + overwrite=False, +): + ee_initialize(gee_account_id) + if aez is None: + for aez in range(20): + compute_spei(aez, end_year, overwrite, start_year) + for spei_type in [1, 3, 12]: + merge_SPEI_rasters(spei_type) + else: + compute_spei(aez, end_year, overwrite, start_year) + + +def compute_spei(aez, end_year, overwrite, start_year): + download_data_locally( + aez=aez, + start_year=start_year, + end_year=end_year, + frequency="monthly", + datasets=None, + overwrite=overwrite, + ) + ppet_multiband( + aez=aez, + start=start_year, + end=end_year, + ) + run_spei(aez, start_year, end_year) + + +@app.task(bind=True) +def run_drought_resistance_resilience( + self, aez, start_year=None, end_year=None, gee_account_id=None +): + task_id, asset_id = generate_hybrid_tree_mask( + aez, start_year=start_year, end_year=end_year, gee_account_id=gee_account_id + ) + if task_id: + check_task_status([task_id]) + + if is_gee_asset_exists(asset_id): + generate_drought_resistance( + aez, start_year=2004, end_year=end_year, gee_account_id=gee_account_id + ) + + +@app.task(bind=True) +def run_rainfall_resistance_resilience( + self, aez, start_year=None, end_year=None, gee_account_id=None +): + task_id = rainfall_index( + aez, start_year=start_year, end_year=end_year, gee_account_id=gee_account_id + ) + if task_id: + check_task_status([task_id]) + + task_id, asset_id = generate_hybrid_tree_mask( + aez, start_year=start_year, end_year=end_year, gee_account_id=gee_account_id + ) + if task_id: + check_task_status([task_id]) + + if is_gee_asset_exists(asset_id): + generate_rainfall_resilience( + aez, start_year=2004, end_year=end_year, gee_account_id=gee_account_id + ) + + +@app.task(bind=True) +def run_forest_fire_resistance_resilience( + self, aez, start_year=None, end_year=None, gee_account_id=None +): + task_id = fire_index( + aez, start_year=start_year, end_year=end_year, gee_account_id=gee_account_id + ) + if task_id: + check_task_status([task_id]) + + task_id, asset_id = generate_hybrid_tree_mask( + aez, start_year=start_year, end_year=end_year, gee_account_id=gee_account_id + ) + if task_id: + check_task_status([task_id]) + + if is_gee_asset_exists(asset_id): + forest_fire_sensitivity( + aez, start_year=2004, end_year=end_year, gee_account_id=gee_account_id + ) + + +@app.task(bind=True) +def run_high_wind_resistance_resilience( + self, aez, start_year=None, end_year=None, gee_account_id=None +): + task_id = max_wind_index( + aez, start_year=start_year, end_year=end_year, gee_account_id=gee_account_id + ) + if task_id: + check_task_status([task_id]) + + task_id, asset_id = generate_hybrid_tree_mask( + aez, start_year=start_year, end_year=end_year, gee_account_id=gee_account_id + ) + if task_id: + check_task_status([task_id]) + + if is_gee_asset_exists(asset_id): + high_wind_sensitivity( + aez, start_year=2004, end_year=end_year, gee_account_id=gee_account_id + ) diff --git a/computing/stac_layer_resolution.py b/computing/stac_layer_resolution.py new file mode 100644 index 00000000..a3f6e6c7 --- /dev/null +++ b/computing/stac_layer_resolution.py @@ -0,0 +1,232 @@ +"""Resolve Layer rows to STAC generation parameters via LayerMapping.""" + +from __future__ import annotations + +import logging +import os +import re +from types import SimpleNamespace + +import pandas as pd +from django.conf import settings + +from computing.models import Layer, LayerMapping +from computing.STAC_specs import constants +from computing.STAC_specs.stac_collection import STACConfig +from utilities.gee_utils import valid_gee_text + +log = logging.getLogger(__name__) + +_LULC_DATASETS = frozenset({"LULC_level_1", "LULC_level_2", "LULC_level_3"}) +_LULC_LAYER_RE = re.compile(r"^LULC_(\d{2})_(\d{2})_", re.IGNORECASE) + + +def _two_digit_hydrological_year(value) -> str: + text = str(value or "").strip() + if not text: + return "" + if len(text) == 2 and text.isdigit(): + return text + if len(text) == 4 and text.isdigit(): + return str(int(text) % 100).zfill(2) + return text + + +def lulc_years_from_layer_name(layer_name: str): + match = _LULC_LAYER_RE.match(layer_name or "") + if not match: + return None, None + return match.group(1), match.group(2) + + +def lulc_calendar_start_year(layer_name: str): + start_yy, _ = lulc_years_from_layer_name(layer_name) + if not start_yy: + return "" + return str(2000 + int(start_yy)) + + +def format_geoserver_name(template: str, layer: Layer) -> str: + if not template: + return "" + misc = layer.misc or {} + start_year = str(misc.get("start_year", "") or "") + end_year = str(misc.get("end_year", "") or "") + lulc_start, lulc_end = lulc_years_from_layer_name(layer.layer_name or "") + if lulc_start: + start_year = lulc_start + end_year = lulc_end or lulc_start + elif "{start_year}" in template: + start_year = _two_digit_hydrological_year(start_year) + end_year = _two_digit_hydrological_year(end_year or start_year) + try: + return template.format( + district=valid_gee_text(layer.district.district_name.lower()), + block=valid_gee_text(layer.block.tehsil_name.lower()), + state=valid_gee_text(layer.state.state_name.lower()), + start_year=start_year, + end_year=end_year, + ) + except (KeyError, IndexError, AttributeError): + return "" + + +def _mapping_from_csv_row(row) -> SimpleNamespace: + return SimpleNamespace( + layer_type=str(row["layer_type"]), + layer_name=str(row["layer_name"]), + ) + + +def _load_layer_mapping_csv(): + config = STACConfig() + path = config.layer_map_csv + if os.path.exists(path): + return pd.read_csv(path) + os.makedirs(os.path.dirname(path), exist_ok=True) + df = pd.read_csv(constants.LAYER_MAP_GITHUB_URL) + df.to_csv(path, index=False) + return df + + +def _resolve_mapping_from_csv(layer: Layer): + dataset_name = (layer.dataset.name or "").strip() + if not dataset_name: + return None + + try: + df = _load_layer_mapping_csv() + except Exception as exc: + log.warning("STAC mapping CSV load failed: %s", exc) + return None + + if "db_dataset_name" not in df.columns: + return None + + candidates = df[df["db_dataset_name"].astype(str).str.strip() == dataset_name] + if candidates.empty: + return None + if len(candidates) == 1: + return _mapping_from_csv_row(candidates.iloc[0]) + + layer_name = (layer.layer_name or "").strip() + if not layer_name: + return None + + matches = [] + for _, row in candidates.iterrows(): + template = str(row.get("geoserver_layer_name", "") or "") + if format_geoserver_name(template, layer) == layer_name: + matches.append(row) + if len(matches) == 1: + return _mapping_from_csv_row(matches[0]) + if not matches: + log.warning( + "STAC mapping CSV: no match for layer id=%s dataset=%s name=%s", + layer.id, + dataset_name, + layer_name, + ) + return None + return _mapping_from_csv_row(matches[0]) + + +def resolve_layer_mapping(layer: Layer): + if not layer.dataset_id: + return None + + dataset_name = (layer.dataset.name or "").strip() + if not dataset_name: + return None + + if dataset_name in _LULC_DATASETS and dataset_name != "LULC_level_3": + # Only level_3 is catalogued for STAC; level_1/level_2 share GeoServer naming. + return None + + # Only match within this dataset. Never fall back to all auto_stac rows — + # Admin Boundary / Drainage / NREGA all share `{district}_{block}` templates. + try: + candidates = list( + LayerMapping.objects.filter(db_dataset_name=dataset_name, auto_stac=True) + ) + except Exception as exc: # noqa: BLE001 — missing table / migrations + log.warning( + "STAC mapping DB unavailable (%s); falling back to CSV for dataset=%s", + exc, + dataset_name, + ) + return _resolve_mapping_from_csv(layer) + + if not candidates: + return _resolve_mapping_from_csv(layer) + + if len(candidates) == 1: + return candidates[0] + + layer_name = (layer.layer_name or "").strip() + if not layer_name: + return _resolve_mapping_from_csv(layer) + + matches = [ + c + for c in candidates + if format_geoserver_name(c.geoserver_layer_name, layer) == layer_name + ] + if len(matches) == 1: + return matches[0] + if not matches: + log.warning( + "STAC mapping DB: no match for layer id=%s dataset=%s name=%s", + layer.id, + dataset_name, + layer_name, + ) + return _resolve_mapping_from_csv(layer) + + log.info( + "STAC mapping DB: %d ambiguous matches for layer id=%s; picking first", + len(matches), + layer.id, + ) + return matches[0] + + +def stac_task_kwargs_for_layer(layer: Layer, mapping=None): + mapping = mapping or resolve_layer_mapping(layer) + if mapping is None: + return None + + misc = layer.misc or {} + start_year = str(misc.get("start_year", "") or "") + end_year = str(misc.get("end_year", "") or "") + lulc_year = lulc_calendar_start_year(layer.layer_name or "") + if lulc_year: + start_year = lulc_year + end_year = lulc_year + return { + "layer_type": mapping.layer_type, + "state": layer.state.state_name, + "district": layer.district.district_name, + "block": layer.block.tehsil_name, + "layer_name": mapping.layer_name, + "start_year": start_year, + "end_year": end_year, + "upload_to_s3": bool(getattr(settings, "STAC_UPLOAD_TO_S3", False)), + "overwrite_metadata": bool(getattr(settings, "STAC_OVERWRITE_METADATA", True)), + "layer_id": layer.id, + } + + +def stac_collect_target_for_layer(layer: Layer): + task_kwargs = stac_task_kwargs_for_layer(layer) + if task_kwargs is None: + return None + return { + "state": task_kwargs["state"], + "district": task_kwargs["district"], + "block": task_kwargs["block"], + "layer_name": task_kwargs["layer_name"], + "layer_type": task_kwargs["layer_type"], + "start_year": task_kwargs["start_year"], + "end_year": task_kwargs["end_year"], + } diff --git a/computing/surface_water_bodies/area_utils.py b/computing/surface_water_bodies/area_utils.py new file mode 100644 index 00000000..a5d66ea5 --- /dev/null +++ b/computing/surface_water_bodies/area_utils.py @@ -0,0 +1,76 @@ +import ee +import pandas as pd + + +SQM_PER_HECTARE = 10000.0 +AREA_YEARS = tuple(range(2018, 2026)) + + +def add_area_ored_to_gdf(gdf): + result = gdf.copy() + area_ored = ( + pd.to_numeric(result["area_ored"], errors="coerce") + if "area_ored" in result + else pd.Series(float("nan"), index=result.index, dtype=float) + ) + + for year in reversed(AREA_YEARS): + area_column = f"area_{year}" + k_column = f"k_{year}" + if area_column not in result or k_column not in result: + continue + + area = pd.to_numeric(result[area_column], errors="coerce") + k = pd.to_numeric(result[k_column], errors="coerce") + candidate = area.multiply(100).divide(k).divide(SQM_PER_HECTARE) + area_ored = area_ored.fillna(candidate.where(k.notna() & k.ne(0))) + + result["area_ored"] = area_ored + return result + + +def _gee_number_and_presence(feature, property_name): + value = feature.get(property_name) + missing = ee.Algorithms.IsEqual(value, None) + number = ee.Number(ee.Algorithms.If(missing, 0, value)) + present = ee.Number(ee.Algorithms.If(missing, 0, 1)).eq(1) + return number, present + + +def gee_area_ored(feature): + feature = ee.Feature(feature) + geometry_area = feature.geometry().area(maxError=1).divide(SQM_PER_HECTARE) + + total_area_m2, has_total_area_m2 = _gee_number_and_presence( + feature, "total_area_m2" + ) + total_area, has_total_area = _gee_number_and_presence(feature, "total_area") + annual_area_divisor = ee.Number( + ee.Algorithms.If(has_total_area_m2, SQM_PER_HECTARE, 1) + ) + derived = ee.Number( + ee.Algorithms.If( + has_total_area, + total_area, + ee.Algorithms.If( + has_total_area_m2, + total_area_m2.divide(SQM_PER_HECTARE), + geometry_area, + ), + ) + ) + + for year in AREA_YEARS: + area, has_area = _gee_number_and_presence(feature, f"area_{year}") + k, has_k = _gee_number_and_presence(feature, f"k_{year}") + valid = has_area.And(has_k).And(k.neq(0)) + candidate = area.multiply(100).divide(k).divide(annual_area_divisor) + derived = ee.Number(ee.Algorithms.If(valid, candidate, derived)) + + existing, has_existing = _gee_number_and_presence(feature, "area_ored") + return ee.Number(ee.Algorithms.If(has_existing, existing, derived)) + + +def ensure_gee_area_ored(feature): + feature = ee.Feature(feature) + return feature.set("area_ored", gee_area_ored(feature)) diff --git a/computing/surface_water_bodies/clip_swb_local.py b/computing/surface_water_bodies/clip_swb_local.py new file mode 100644 index 00000000..ed15c10b --- /dev/null +++ b/computing/surface_water_bodies/clip_swb_local.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import logging +import os +from typing import Any + +import geopandas as gpd +import shapely +from shapely.geometry import shape +from shapely.geometry.base import BaseGeometry +from shapely.ops import unary_union + +logger = logging.getLogger(__name__) + + +def _to_geom(roi: Any) -> BaseGeometry: + """Normalize a GeoJSON ROI payload into a valid Shapely geometry.""" + if isinstance(roi, BaseGeometry): + geometry = roi + elif isinstance(roi, dict): + roi_type = roi.get("type") + if roi_type == "Feature": + geometry = shape(roi["geometry"]) + elif roi_type == "FeatureCollection": + features = roi.get("features") or [] + if not features: + raise ValueError("Empty FeatureCollection") + geometry = unary_union([shape(feature["geometry"]) for feature in features]) + else: + geometry = shape(roi) + else: + raise ValueError("ROI must be a GeoJSON object") + + if not geometry.is_valid: + geometry = shapely.make_valid(geometry) + if geometry.is_empty: + raise ValueError("Empty ROI geometry") + return geometry + + +def _clip_gdf(swb_path: str, roi: BaseGeometry) -> gpd.GeoDataFrame: + """Clip SWB features against the ROI after a bbox-prefiltered read.""" + minx, miny, maxx, maxy = roi.bounds + bbox = (minx, miny, maxx, maxy) + logger.info("Reading SWB source for clipping: path=%s bbox=%s", swb_path, bbox) + + previous_max_obj_size = os.environ.get("OGR_GEOJSON_MAX_OBJ_SIZE") + os.environ["OGR_GEOJSON_MAX_OBJ_SIZE"] = "0" + try: + gdf = gpd.read_file(swb_path, bbox=bbox, engine="pyogrio") + finally: + if previous_max_obj_size is None: + os.environ.pop("OGR_GEOJSON_MAX_OBJ_SIZE", None) + else: + os.environ["OGR_GEOJSON_MAX_OBJ_SIZE"] = previous_max_obj_size + + if gdf.empty: + logger.info("No SWB features found in bbox for source=%s", swb_path) + return gdf + + if gdf.crs is not None and gdf.crs.to_epsg() != 4326: + gdf = gdf.to_crs(4326) + + clipped = gdf[gdf.geometry.intersects(roi)] + if clipped.empty: + logger.info("No SWB features intersect ROI after bbox read for source=%s", swb_path) + return clipped + + clipped = clipped.copy() + clipped["geometry"] = clipped.geometry.intersection(roi) + clipped = clipped[~clipped.geometry.is_empty].reset_index(drop=True) + logger.info("Finished SWB clipping: retained_features=%s", len(clipped)) + return clipped diff --git a/computing/surface_water_bodies/swb3.py b/computing/surface_water_bodies/swb3.py index 2f3ea7d8..7b1281f7 100644 --- a/computing/surface_water_bodies/swb3.py +++ b/computing/surface_water_bodies/swb3.py @@ -1,5 +1,11 @@ from computing.utils import generate_swb_layer_with_max_so_catchment -from utilities.constants import GEE_PATHS +from computing.surface_water_bodies.area_utils import ensure_gee_area_ored +from utilities.constants import ( + CATCHMENT_AREA, + GEE_PATHS, + PAN_INDIA_DRAINAGE_LINES_DATASET, + STREAM_ORDER_ASSET, +) from utilities.gee_utils import ( valid_gee_text, get_gee_dir_path, @@ -232,6 +238,10 @@ def waterbody_catchment_streamorder_properties( river_asset_id=DEFAULT_PAN_INDIA_RIVER_ASSET, canal_asset_id=DEFAULT_PAN_INDIA_CANAL_ASSET, waterbody_type_buffer_m=DEFAULT_WATERBODY_TYPE_BUFFER_M, + swb2_asset_suffix=None, + stream_order_asset_id=STREAM_ORDER_ASSET, + catchment_area_asset_id=CATCHMENT_AREA, + drainage_lines_asset_id=PAN_INDIA_DRAINAGE_LINES_DATASET, ): print(f"asset suffix swb3: {asset_suffix}") print(f"[SWB4] river_asset_id: {river_asset_id}") @@ -245,32 +255,30 @@ def waterbody_catchment_streamorder_properties( ) + description ) + if is_gee_asset_exists(asset_id): + return None, asset_id + + swb2_asset_suffix = swb2_asset_suffix or asset_suffix swb2_asset = ( get_gee_dir_path( asset_folder_list, asset_path=GEE_PATHS[app_type]["GEE_ASSET_PATH"] ) + "swb2_" - + asset_suffix + + swb2_asset_suffix ) # As requested: SWB3 always uses SWB2 as input. - water_bodies = ee.FeatureCollection(swb2_asset) + water_bodies = ee.FeatureCollection(swb2_asset).map(ensure_gee_area_ored) print(f"asset_i{water_bodies}") swb4_fs = generate_swb_layer_with_max_so_catchment( roi=water_bodies, - asset_suffix=asset_suffix, - asset_folder=asset_folder_list, - app_type=app_type, gee_account_id=gee_account_id, + stream_order_asset_id=stream_order_asset_id, + catchment_area_asset_id=catchment_area_asset_id, ) - asset_id_dl = ( - get_gee_dir_path( - asset_folder_list, asset_path=GEE_PATHS[app_type]["GEE_ASSET_PATH"] - ) - + "drainage_lines_" - + asset_suffix + swb4_fs_on_drainage = add_on_drainage_flag( + swb4_fs, drainage_lines_asset_id ) - swb4_fs_on_drainage = add_on_drainage_flag(swb4_fs, asset_id_dl) swb4_fc_with_waterbody_type = add_waterbody_type_flag( swb4_fs_on_drainage, river_asset_id=river_asset_id, diff --git a/computing/surface_water_bodies/swb4.py b/computing/surface_water_bodies/swb4.py index afb64ebb..dcb05eb7 100644 --- a/computing/surface_water_bodies/swb4.py +++ b/computing/surface_water_bodies/swb4.py @@ -1,5 +1,6 @@ import ee +from computing.surface_water_bodies.area_utils import ensure_gee_area_ored from utilities.constants import GEE_PATHS, WBC from utilities.gee_utils import ( get_gee_dir_path, @@ -40,7 +41,18 @@ def waterbody_wbc_intersection( ) + "swb3_" + asset_suffix - ) + ).map(ensure_gee_area_ored) + + def ensure_uid(feature): + feature = ee.Feature(feature) + uid = ee.Algorithms.If( + ee.Algorithms.IsEqual(feature.get("UID"), None), + feature.get("wb_id"), + feature.get("UID"), + ) + return feature.set("UID", uid) + + water_bodies = water_bodies.map(ensure_uid) # Filter points and polygons within the area of interest (aoi) points = census_state.filterBounds(roi) @@ -92,7 +104,6 @@ def replace_null_area(feature): def select_closest_polygon(feature): # Calculate the spread area of the water body spread = ee.Number(feature.get("water_spread_area_of_water_body")) - spread = spread.multiply(10000) intersections = ee.List(feature.get("intersections")) # Calculate the difference between intersection areas diff --git a/computing/surface_water_bodies/swb_local.py b/computing/surface_water_bodies/swb_local.py new file mode 100644 index 00000000..3e5cb8f6 --- /dev/null +++ b/computing/surface_water_bodies/swb_local.py @@ -0,0 +1,739 @@ +import logging +from pathlib import Path + +import ee +import geopandas as gpd + +from computing.config_loader import ( + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + SWB_VECTOR_OUTPUT_DIR as LOCAL_OUTPUT_BASE_DIR, + SWB_VECTOR_PATH, +) +from computing.local_compute_helper import ( + build_output_vector_path, + load_precomputed_roi, + read_validated_vector_file, + write_vector_output, +) +from computing.surface_water_bodies.area_utils import ( + SQM_PER_HECTARE, + add_area_ored_to_gdf, +) +from computing.surface_water_bodies.clip_swb_local import _clip_gdf, _to_geom +from computing.surface_water_bodies.swb3 import ( + waterbody_catchment_streamorder_properties, +) +from computing.surface_water_bodies.swb4 import waterbody_wbc_intersection +from computing.utils import ( + push_shape_to_geoserver, + save_layer_info_to_db, + sync_fc_to_geoserver, + update_layer_sync_status, +) +from nrm_app.celery import app +from utilities.constants import GEE_PATHS +from utilities.gee_utils import ( + check_task_status, + create_gee_dir, + ee_initialize, + export_vector_asset_to_gee, + gdf_to_ee_fc, + get_gee_dir_path, + is_gee_asset_exists, + make_asset_public, + valid_gee_text, +) + +LOCAL_ALGORITHM = "local_surface_water_bodies_clip" +LOCAL_ALGORITHM_VERSION = "local-1.0" +DATASET_NAME = "Surface Water Bodies" +GEOSERVER_WORKSPACE = "swb" +logger = logging.getLogger(__name__) +GEE_EXPORT_CHUNK_SIZE = 1000 + + +def _slug(value, fallback): + if value is None: + return fallback + return valid_gee_text(str(value).strip().lower()) or fallback + + +def _resolve_source_path(vector_path): + src = Path(vector_path).expanduser().resolve() + candidates = [ + src.parent / "pan_india_waterbodies_with_mws.fgb", + src.with_suffix(".fgb"), + src, + ] + for candidate in candidates: + if candidate.exists(): + logger.info("Using SWB source path: %s", candidate) + return str(candidate) + + raise FileNotFoundError( + "Surface water bodies source not found. Checked: " + + ", ".join(str(candidate) for candidate in candidates) + ) + + +def _resolve_roi_gdf(state, district, block, roi=None, roi_path=None): + if state and district and block: + return load_precomputed_roi( + state=state, + district=district, + block=block, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + ) + + if roi is not None: + geometry = _to_geom(roi) + return gpd.GeoDataFrame({"geometry": [geometry]}, crs="EPSG:4326") + + if roi_path: + return read_validated_vector_file( + roi_path, + f"ROI file has no valid geometries: {roi_path}", + ) + + raise ValueError( + "Provide either state/district/block or a valid `roi`/`roi_path` for local SWB clipping." + ) + + +def _resolve_asset_suffix(state, district, block, asset_suffix): + if state and district and block: + return f"{_slug(district, 'unknown_district')}_{_slug(block, 'unknown_block')}" + if asset_suffix and str(asset_suffix).strip(): + return _slug(asset_suffix, "custom") + raise ValueError( + "For non state/district/block runs, `asset_suffix` must be provided." + ) + + +def _layer_name(asset_suffix): + return f"surface_waterbodies_{asset_suffix}" + + +def _final_layer_name(asset_suffix): + return _layer_name(asset_suffix) + + +def _gee_description(asset_suffix): + return f"swb2_{asset_suffix}_local" + + +def _resolve_asset_folder_list(state, district, block): + if state and district and block: + return [state, district, block] + return None + + +def _resolve_gee_asset_id(state, district, block, asset_suffix, app_type): + asset_folder_list = _resolve_asset_folder_list(state, district, block) + if not asset_folder_list: + raise ValueError( + "Local SWB GEE export requires state, district, and block so the asset can be saved alongside the existing swb2 assets." + ) + + description = _gee_description(asset_suffix) + asset_id = ( + get_gee_dir_path( + asset_folder_list, asset_path=GEE_PATHS[app_type]["GEE_ASSET_PATH"] + ) + + description + ) + if "projects//" in asset_id: + raise ValueError( + "Invalid GEE asset path resolved. " + f"asset_id={asset_id}. " + "Check GEE_STORAGE_PROJECT / GEE_PATHS configuration." + ) + return description, asset_id, asset_folder_list + + +def _prepare_gdf_for_gee(gdf): + prepared = gdf.copy() + if prepared.crs is None: + prepared = prepared.set_crs("EPSG:4326") + elif prepared.crs.to_epsg() != 4326: + prepared = prepared.to_crs("EPSG:4326") + + for column in prepared.columns: + if column == "geometry": + continue + prepared[column] = prepared[column].where(prepared[column].notna(), None) + return prepared + + +def _convert_area_columns_to_hectares(gdf): + converted = add_area_ored_to_gdf(gdf) + converted_columns = {} + + for column in list(converted.columns): + if column == "geometry": + continue + + target_column = None + if column == "total_area_m2": + target_column = "total_area" + elif column.startswith("area_") and column != "area_ored": + target_column = column + + if target_column is None: + continue + + converted[target_column] = converted[column] / SQM_PER_HECTARE + if target_column != column: + converted.drop(columns=[column], inplace=True) + converted_columns[column] = "converted_in_place_to_hectares" + + return converted, converted_columns + + +def _create_local_swb_output(swb_path, roi_geometry, output_path, layer_name): + clipped_gdf = _clip_gdf(_resolve_source_path(swb_path), roi_geometry) + if clipped_gdf.empty: + raise ValueError("No surface water body features intersect the provided ROI.") + + clipped_gdf, converted_area_columns = _convert_area_columns_to_hectares( + clipped_gdf + ) + logger.info( + "Clipped SWB features: count=%s columns=%s", + len(clipped_gdf), + list(clipped_gdf.columns), + ) + logger.info( + "Converted SWB area columns from square meters to hectares: %s", + converted_area_columns, + ) + + local_asset_path = write_vector_output( + gdf=clipped_gdf, + output_path=output_path, + layer_name=layer_name, + ) + logger.info("Saved local SWB vector to disk: %s", local_asset_path) + return clipped_gdf, local_asset_path + + +def _union_geometry(gdf): + try: + return gdf.union_all() + except AttributeError: + return gdf.geometry.unary_union + + +def _chunk_asset_id(base_asset_id, index): + return f"{base_asset_id}_chunk_{index:04d}" + + +def _chunk_description(base_description, index): + return f"{base_description}_chunk_{index:04d}" + + +def _export_gdf_to_gee_in_chunks(gdf, base_description, base_asset_id): + chunk_asset_ids = [] + task_ids = [] + + for index, start in enumerate(range(0, len(gdf), GEE_EXPORT_CHUNK_SIZE)): + chunk = gdf.iloc[start : start + GEE_EXPORT_CHUNK_SIZE].copy() + chunk_description = _chunk_description(base_description, index) + chunk_asset_id = _chunk_asset_id(base_asset_id, index) + logger.info( + "Starting chunked SWB export: chunk_index=%s start=%s end=%s chunk_size=%s chunk_asset_id=%s", + index, + start, + start + len(chunk), + len(chunk), + chunk_asset_id, + ) + chunk_fc = gdf_to_ee_fc(_prepare_gdf_for_gee(chunk)) + task_id = export_vector_asset_to_gee( + chunk_fc, + chunk_description, + chunk_asset_id, + ) + if not task_id: + raise RuntimeError( + f"Failed to start chunk export for {chunk_asset_id}" + ) + chunk_asset_ids.append(chunk_asset_id) + task_ids.append(task_id) + + check_task_status(task_ids) + logger.info( + "Completed all chunk exports for base_asset_id=%s chunk_count=%s", + base_asset_id, + len(chunk_asset_ids), + ) + + merged_fc = ee.FeatureCollection( + [ee.FeatureCollection(chunk_asset_id) for chunk_asset_id in chunk_asset_ids] + ).flatten() + merge_task_id = export_vector_asset_to_gee(merged_fc, base_description, base_asset_id) + if not merge_task_id: + raise RuntimeError(f"Failed to start merged export for {base_asset_id}") + logger.info( + "Started merged SWB export from chunks: task_id=%s gee_asset_id=%s", + merge_task_id, + base_asset_id, + ) + check_task_status([merge_task_id]) + logger.info("Completed merged SWB export: gee_asset_id=%s", base_asset_id) + + if not is_gee_asset_exists(base_asset_id): + raise RuntimeError( + f"Merged SWB asset was not created after chunk merge: {base_asset_id}" + ) + + for chunk_asset_id in chunk_asset_ids: + try: + if is_gee_asset_exists(chunk_asset_id): + ee.data.deleteAsset(chunk_asset_id) + logger.info("Deleted temporary SWB chunk asset: %s", chunk_asset_id) + except Exception as exc: + logger.warning( + "Failed to delete temporary SWB chunk asset %s: %s", + chunk_asset_id, + exc, + ) + + +def _push_local_swb_to_geoserver(output_path, layer_name): + geoserver_response = push_shape_to_geoserver( + str(Path(output_path).with_suffix("")), + workspace=GEOSERVER_WORKSPACE, + layer_name=layer_name, + file_type="gpkg", + ) + logger.info("GeoServer response for local SWB layer %s: %s", layer_name, geoserver_response) + return bool(geoserver_response) and geoserver_response.get("status_code") in ( + 200, + 201, + 202, + ) + + +def _continue_swb_in_gee( + state, + asset_suffix, + asset_folder_list, + app_type, + gee_account_id, + roi, +): + swb2_asset_suffix = f"{asset_suffix}_local" + swb3_task_id, swb3_asset_id = waterbody_catchment_streamorder_properties( + roi=roi, + asset_suffix=asset_suffix, + asset_folder_list=asset_folder_list, + app_type=app_type, + gee_account_id=gee_account_id, + swb2_asset_suffix=swb2_asset_suffix, + ) + if swb3_task_id: + check_task_status([swb3_task_id]) + if not is_gee_asset_exists(swb3_asset_id): + raise RuntimeError(f"SWB3 GEE asset was not created: {swb3_asset_id}") + + swb4_task_id, swb4_asset_id = waterbody_wbc_intersection( + roi=roi, + state=state, + asset_suffix=asset_suffix, + asset_folder_list=asset_folder_list, + app_type=app_type, + ) + if swb4_task_id: + check_task_status([swb4_task_id]) + if not is_gee_asset_exists(swb4_asset_id): + raise RuntimeError(f"SWB4 GEE asset was not created: {swb4_asset_id}") + + make_asset_public(swb3_asset_id) + make_asset_public(swb4_asset_id) + return asset_suffix, swb3_asset_id + + +def _sync_final_swb( + state, + district, + block, + layer_name, + asset_suffix, + asset_id, + start_year, + end_year, + push_to_geoserver, + sync_layer_metadata, +): + layer_id = None + if sync_layer_metadata: + misc = { + "is_generated_locally": True, + "source_stage": "swb3_gee_from_swb2_local", + } + if start_year is not None: + misc["start_year"] = start_year + if end_year is not None: + misc["end_year"] = end_year + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name=DATASET_NAME, + misc=misc, + algorithm=LOCAL_ALGORITHM, + algorithm_version=LOCAL_ALGORITHM_VERSION, + ) + + if not push_to_geoserver: + return True + + response = sync_fc_to_geoserver( + ee.FeatureCollection(asset_id), + asset_suffix, + layer_name, + workspace=GEOSERVER_WORKSPACE, + ) + synced = bool(response) and response.get("status_code") in (200, 201, 202) + if not synced: + raise RuntimeError(f"Failed to sync final SWB layer to GeoServer: {layer_name}") + if synced and layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + return synced + + +def _complete_swb_pipeline( + state, + district, + block, + asset_suffix, + asset_folder_list, + app_type, + gee_account_id, + roi, + start_year, + end_year, + push_to_geoserver, + sync_layer_metadata, +): + final_asset_suffix, final_asset_id = _continue_swb_in_gee( + state=state, + asset_suffix=asset_suffix, + asset_folder_list=asset_folder_list, + app_type=app_type, + gee_account_id=gee_account_id, + roi=roi, + ) + return _sync_final_swb( + state=state, + district=district, + block=block, + layer_name=_final_layer_name(asset_suffix), + asset_suffix=final_asset_suffix, + asset_id=final_asset_id, + start_year=start_year, + end_year=end_year, + push_to_geoserver=push_to_geoserver, + sync_layer_metadata=sync_layer_metadata, + ) + + +def run_swb_local( + state=None, + district=None, + block=None, + roi=None, + roi_path=None, + asset_suffix=None, + swb_path=SWB_VECTOR_PATH, + push_to_geoserver=True, + sync_layer_metadata=True, + gee_account_id=None, + app_type="MWS", + start_year=None, + end_year=None, +): + state = str(state).strip().lower() if state else None + district = str(district).strip().lower() if district else None + block = str(block).strip().lower() if block else None + + roi_gdf = _resolve_roi_gdf( + state=state, + district=district, + block=block, + roi=roi, + roi_path=roi_path, + ) + logger.info( + "Starting local SWB generation: state=%s district=%s block=%s asset_suffix=%s app_type=%s", + state, + district, + block, + asset_suffix, + app_type, + ) + logger.info("Resolved ROI rows=%s crs=%s", len(roi_gdf), roi_gdf.crs) + roi_geometry = _union_geometry(roi_gdf) + if roi_geometry.is_empty: + raise ValueError("ROI geometry is empty after validation.") + + asset_suffix = _resolve_asset_suffix(state, district, block, asset_suffix) + layer_name = _layer_name(asset_suffix) + gee_description, gee_asset_id, asset_folder_list = _resolve_gee_asset_id( + state=state, + district=district, + block=block, + asset_suffix=asset_suffix, + app_type=app_type, + ) + logger.info( + "Resolved local SWB names: layer_name=%s gee_description=%s gee_asset_id=%s asset_folder_list=%s", + layer_name, + gee_description, + gee_asset_id, + asset_folder_list, + ) + + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + custom_subdir=asset_suffix, + block_fallback="unknown_block", + ) + logger.info("Local SWB output path: %s", output_path) + + ee_initialize(gee_account_id) + gee_roi = gdf_to_ee_fc(_prepare_gdf_for_gee(roi_gdf[["geometry"]])) + + if is_gee_asset_exists(gee_asset_id): + logger.info("GEE asset already exists, reusing: %s", gee_asset_id) + if output_path.exists(): + local_asset_path = str(output_path) + else: + _, local_asset_path = _create_local_swb_output( + swb_path=swb_path, + roi_geometry=roi_geometry, + output_path=output_path, + layer_name=layer_name, + ) + + layer_id = None + if sync_layer_metadata: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=gee_asset_id, + dataset_name=DATASET_NAME, + misc={ + "is_generated_locally": True, + "local_vector_path": local_asset_path, + "source_stage": "swb2_local", + }, + algorithm=LOCAL_ALGORITHM, + algorithm_version=LOCAL_ALGORITHM_VERSION, + ) + make_asset_public(gee_asset_id) + + if push_to_geoserver: + layer_at_geoserver = _push_local_swb_to_geoserver( + output_path=local_asset_path, layer_name=layer_name + ) + if not layer_at_geoserver: + raise RuntimeError( + f"Failed to sync SWB2 layer to GeoServer: {layer_name}" + ) + if layer_at_geoserver and layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + logger.info("Sync to GeoServer flag updated for existing local SWB layer") + return _complete_swb_pipeline( + state=state, + district=district, + block=block, + asset_suffix=asset_suffix, + asset_folder_list=asset_folder_list, + app_type=app_type, + gee_account_id=gee_account_id, + roi=gee_roi, + start_year=start_year, + end_year=end_year, + push_to_geoserver=push_to_geoserver, + sync_layer_metadata=sync_layer_metadata, + ) + + clipped_gdf, local_asset_path = _create_local_swb_output( + swb_path=swb_path, + roi_geometry=roi_geometry, + output_path=output_path, + layer_name=layer_name, + ) + + logger.info( + "Initialized Earth Engine for local SWB export: gee_account_id=%s", + gee_account_id, + ) + create_gee_dir(asset_folder_list, GEE_PATHS[app_type]["GEE_ASSET_PATH"]) + logger.info( + "Ensured GEE directory exists under base path=%s for folders=%s", + GEE_PATHS[app_type]["GEE_ASSET_PATH"], + asset_folder_list, + ) + ee_fc = gdf_to_ee_fc(_prepare_gdf_for_gee(clipped_gdf)) + logger.info("Prepared EE FeatureCollection for export: gee_asset_id=%s", gee_asset_id) + task_id = None + try: + task_id = export_vector_asset_to_gee(ee_fc, gee_description, gee_asset_id) + except Exception as exc: + logger.warning( + "Inline GEE export raised an exception for asset_id=%s: %s", + gee_asset_id, + exc, + ) + + if task_id: + logger.info( + "Started GEE export task: task_id=%s gee_description=%s gee_asset_id=%s", + task_id, + gee_description, + gee_asset_id, + ) + check_task_status([task_id]) + logger.info( + "Completed GEE export task: task_id=%s gee_asset_id=%s", + task_id, + gee_asset_id, + ) + else: + logger.info( + "Inline GEE export could not be started for asset_id=%s. Falling back to chunked FeatureCollection export.", + gee_asset_id, + ) + _export_gdf_to_gee_in_chunks( + gdf=clipped_gdf, + base_description=gee_description, + base_asset_id=gee_asset_id, + ) + logger.info( + "Triggered chunked SWB GEE export fallback: gee_asset_id=%s chunk_size=%s", + gee_asset_id, + GEE_EXPORT_CHUNK_SIZE, + ) + + if not is_gee_asset_exists(gee_asset_id): + raise RuntimeError(f"GEE asset was not created: {gee_asset_id}") + make_asset_public(gee_asset_id) + logger.info("Marked GEE asset public: %s", gee_asset_id) + + layer_id = None + if sync_layer_metadata: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=gee_asset_id, + dataset_name=DATASET_NAME, + misc={ + "is_generated_locally": True, + "feature_count": int(len(clipped_gdf)), + "local_vector_path": local_asset_path, + "source_stage": "swb2_local", + }, + algorithm=LOCAL_ALGORITHM, + algorithm_version=LOCAL_ALGORITHM_VERSION, + ) + logger.info( + "Saved local SWB layer metadata: layer_id=%s gee_asset_id=%s layer_name=%s", + layer_id, + gee_asset_id, + layer_name, + ) + + if push_to_geoserver: + layer_at_geoserver = _push_local_swb_to_geoserver( + output_path=local_asset_path, layer_name=layer_name + ) + if not layer_at_geoserver: + raise RuntimeError(f"Failed to sync SWB2 layer to GeoServer: {layer_name}") + if layer_at_geoserver and layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + logger.info("Sync to GeoServer flag updated for local SWB layer: %s", layer_name) + + return _complete_swb_pipeline( + state=state, + district=district, + block=block, + asset_suffix=asset_suffix, + asset_folder_list=asset_folder_list, + app_type=app_type, + gee_account_id=gee_account_id, + roi=gee_roi, + start_year=start_year, + end_year=end_year, + push_to_geoserver=push_to_geoserver, + sync_layer_metadata=sync_layer_metadata, + ) + + +def _generate_swb_local_task( + state=None, + district=None, + block=None, + roi=None, + roi_path=None, + asset_suffix=None, + start_year=None, + end_year=None, + gee_account_id=None, + app_type="MWS", +): + return run_swb_local( + state=state, + district=district, + block=block, + roi=roi, + roi_path=roi_path, + asset_suffix=asset_suffix, + push_to_geoserver=True, + sync_layer_metadata=True, + gee_account_id=gee_account_id, + app_type=app_type, + start_year=start_year, + end_year=end_year, + ) + + +@app.task(bind=True) +def generate_swb_layer( + self, + state=None, + district=None, + block=None, + roi=None, + roi_path=None, + asset_suffix=None, + start_year=None, + end_year=None, + gee_account_id=None, + app_type="MWS", +): + _ = self + return _generate_swb_local_task( + state=state, + district=district, + block=block, + roi=roi, + roi_path=roi_path, + asset_suffix=asset_suffix, + start_year=start_year, + end_year=end_year, + gee_account_id=gee_account_id, + app_type=app_type, + ) diff --git a/computing/tasks.py b/computing/tasks.py index 910ae270..71f0c21a 100644 --- a/computing/tasks.py +++ b/computing/tasks.py @@ -1,3 +1,40 @@ +import logging + +from nrm_app.celery import app + from computing.STAC_specs.stac_collection import generate_stac_collection_task +from computing.bulk_layer_generation import run_pipeline + + +logger = logging.getLogger(__name__) + + +@app.task(name="computing.tasks.bulk_generate_layer") +def bulk_generate_layer( + pipeline, + location, + overwrite=True, + compute="local", + start_year=None, + end_year=None, + gee_account_id=None, +): + logger.info( + "Running bulk pipeline %s for %s/%s/%s", + pipeline, + location["state"], + location["district"], + location["block"], + ) + return run_pipeline( + pipeline, + location, + overwrite=overwrite, + compute=compute, + start_year=start_year, + end_year=end_year, + gee_account_id=gee_account_id, + ) + -__all__ = ["generate_stac_collection_task"] +__all__ = ["bulk_generate_layer", "generate_stac_collection_task"] diff --git a/computing/terrain_descriptor/store_watersheds_for_tehsils.py b/computing/terrain_descriptor/store_watersheds_for_tehsils.py new file mode 100644 index 00000000..6986b4f0 --- /dev/null +++ b/computing/terrain_descriptor/store_watersheds_for_tehsils.py @@ -0,0 +1,445 @@ +import argparse +import csv +import os +import re +import time + +import geopandas as gpd + + +DEFAULT_MICROWATERSHED_PATH = ( + "data/base_layers/Microwatershed_v2_with_details.geojson" +) +DEFAULT_TEHSIL_PATH = "data/admin-boundary/input/soi_tehsil.geojson" +DEFAULT_OUTPUT_DIR = "data/base_layers/tehsil_watersheds" + +STATE_COLUMN_CANDIDATES = ["STATE", "state", "state_name", "State"] +DISTRICT_COLUMN_CANDIDATES = ["District", "district", "district_name", "DISTRICT"] +TEHSIL_COLUMN_CANDIDATES = ["TEHSIL", "tehsil", "tehsil_name", "block", "block_name"] +MWS_UID_COLUMN_CANDIDATES = ["uid", "UID", "Uid"] +MWS_OPTIONAL_COLUMNS = ["area_in_ha", "bacode", "sbcode", "wsconc"] + +OUTPUT_FORMATS = { + "geojson": ("GeoJSON", ".geojson"), + "gpkg": ("GPKG", ".gpkg"), +} + + +def valid_gee_text(description): + description = re.sub(r"[^a-zA-Z0-9 ,:;_-]", "", description) + return description.replace(" ", "_") + + +def _normalize_text(value): + return valid_gee_text(str(value).strip().lower()) + +def _find_matching_column(columns, candidates): + columns_list = list(columns) + lower_lookup = {col.lower(): col for col in columns_list} + for candidate in candidates: + if candidate.lower() in lower_lookup: + return lower_lookup[candidate.lower()] + + compact_lookup = { + re.sub(r"[^a-z0-9]", "", col.lower()): col for col in columns_list + } + for candidate in candidates: + compact = re.sub(r"[^a-z0-9]", "", candidate.lower()) + if compact in compact_lookup: + return compact_lookup[compact] + return None + + +def _validate_geometry(gdf, fix_invalid=False): + gdf = gdf[gdf.geometry.notna()].copy() + if gdf.empty: + return gdf + + gdf = gdf[~gdf.geometry.is_empty].copy() + if gdf.empty: + return gdf + + if fix_invalid: + invalid = ~gdf.is_valid + if invalid.any(): + gdf.loc[invalid, "geometry"] = gdf.loc[invalid, "geometry"].buffer(0) + gdf = gdf[gdf.geometry.notna() & ~gdf.geometry.is_empty].copy() + return gdf + + +def _prepare_tehsil_boundaries(tehsil_path, state=None, district=None, tehsil=None): + tehsil_gdf = gpd.read_file(tehsil_path) + + state_col = "STATE" if "STATE" in tehsil_gdf.columns else None + district_col = "District" if "District" in tehsil_gdf.columns else None + tehsil_col = "TEHSIL" if "TEHSIL" in tehsil_gdf.columns else None + + if state_col is None: + state_col = _find_matching_column(tehsil_gdf.columns, STATE_COLUMN_CANDIDATES) + if district_col is None: + district_col = _find_matching_column(tehsil_gdf.columns, DISTRICT_COLUMN_CANDIDATES) + if tehsil_col is None: + tehsil_col = _find_matching_column(tehsil_gdf.columns, TEHSIL_COLUMN_CANDIDATES) + + if not all([state_col, district_col, tehsil_col]): + raise ValueError( + "Could not identify STATE/District/TEHSIL columns in tehsil file. " + f"Available columns: {list(tehsil_gdf.columns)}" + ) + + if state: + tehsil_gdf = tehsil_gdf[ + tehsil_gdf[state_col].fillna("").map(_normalize_text) + == _normalize_text(state) + ] + if district: + tehsil_gdf = tehsil_gdf[ + tehsil_gdf[district_col].fillna("").map(_normalize_text) + == _normalize_text(district) + ] + if tehsil: + tehsil_gdf = tehsil_gdf[ + tehsil_gdf[tehsil_col].fillna("").map(_normalize_text) + == _normalize_text(tehsil) + ] + + tehsil_gdf = _validate_geometry(tehsil_gdf, fix_invalid=True) + if tehsil_gdf.empty: + raise ValueError("No matching tehsil records found after applying filters.") + + if tehsil_gdf.crs is None: + tehsil_gdf = tehsil_gdf.set_crs("EPSG:4326") + + tehsil_gdf["__state_norm"] = tehsil_gdf[state_col].map(_normalize_text) + tehsil_gdf["__district_norm"] = tehsil_gdf[district_col].map(_normalize_text) + tehsil_gdf["__tehsil_norm"] = tehsil_gdf[tehsil_col].map(_normalize_text) + tehsil_gdf["__key"] = ( + tehsil_gdf["__state_norm"] + + "||" + + tehsil_gdf["__district_norm"] + + "||" + + tehsil_gdf["__tehsil_norm"] + ) + + meta = ( + tehsil_gdf.groupby("__key", as_index=False) + .agg( + { + state_col: "first", + district_col: "first", + tehsil_col: "first", + } + ) + .rename( + columns={ + state_col: "STATE", + district_col: "District", + tehsil_col: "TEHSIL", + } + ) + ) + + dissolved = tehsil_gdf[["__key", "geometry"]].dissolve(by="__key").reset_index() + dissolved = dissolved.merge(meta, on="__key", how="left") + dissolved = _validate_geometry(dissolved, fix_invalid=True) + + return dissolved + + +def _load_microwatersheds(microwatershed_path, fix_invalid=False): + microwatershed_gdf = gpd.read_file(microwatershed_path) + if microwatershed_gdf.empty: + raise ValueError("Microwatershed file is empty.") + + uid_col = _find_matching_column( + microwatershed_gdf.columns, MWS_UID_COLUMN_CANDIDATES + ) + if not uid_col: + raise ValueError( + "Could not identify microwatershed UID column. " + f"Available columns: {list(microwatershed_gdf.columns)}" + ) + + keep_columns = ["geometry", uid_col] + for col in MWS_OPTIONAL_COLUMNS: + if col in microwatershed_gdf.columns: + keep_columns.append(col) + + microwatershed_gdf = microwatershed_gdf[keep_columns].copy() + microwatershed_gdf = microwatershed_gdf.rename(columns={uid_col: "uid"}) + microwatershed_gdf = _validate_geometry(microwatershed_gdf, fix_invalid=fix_invalid) + + if microwatershed_gdf.crs is None: + microwatershed_gdf = microwatershed_gdf.set_crs("EPSG:4326") + + if microwatershed_gdf.empty: + raise ValueError("No valid microwatershed geometry found.") + + return microwatershed_gdf + + +def _save_subset(subset_gdf, output_path, driver): + if driver == "GPKG": + subset_gdf.to_file(output_path, driver=driver, layer="watersheds") + else: + subset_gdf.to_file(output_path, driver=driver) + + +def _clip_microwatersheds_to_tehsil(candidates_gdf, tehsil_geom): + clipped_gdf = candidates_gdf.copy() + clipped_gdf["geometry"] = clipped_gdf.geometry.intersection(tehsil_geom) + clipped_gdf = _validate_geometry(clipped_gdf, fix_invalid=False) + + if clipped_gdf.empty: + return clipped_gdf + + # Polygon-on-polygon intersection can still produce line/point results for + # features that only touch the tehsil boundary. Keep only polygonal output. + polygonal_mask = clipped_gdf.geometry.geom_type.isin(["Polygon", "MultiPolygon"]) + clipped_gdf = clipped_gdf[polygonal_mask].copy() + + return clipped_gdf + + +def _select_microwatersheds_for_tehsil(candidates_gdf, tehsil_geom, clip_to_tehsil): + """ + Match GEE `FeatureCollection.filterBounds(...)` by default: + keep full microwatershed features whose geometry intersects the tehsil. + Optional clipping is exposed only for workflows that explicitly need it. + """ + selected_gdf = candidates_gdf[candidates_gdf.intersects(tehsil_geom)].copy() + + if not clip_to_tehsil or selected_gdf.empty: + return selected_gdf + + return _clip_microwatersheds_to_tehsil(selected_gdf, tehsil_geom) + + +def generate_tehsil_watershed_copies( + microwatershed_path=DEFAULT_MICROWATERSHED_PATH, + tehsil_path=DEFAULT_TEHSIL_PATH, + output_dir=DEFAULT_OUTPUT_DIR, + output_format="gpkg", + overwrite=False, + include_empty=False, + fix_invalid_mws=False, + clip_to_tehsil=False, + state=None, + district=None, + tehsil=None, +): + """ + Generate per-tehsil MWS layers. + + Default behavior mirrors the current GEE pipeline in `computing/mws/mws.py`, + where `filterBounds(admin_boundary.geometry())` selects full intersecting + microwatershed features without clipping them to the admin boundary. + """ + if output_format not in OUTPUT_FORMATS: + raise ValueError( + f"Unsupported format: {output_format}. Supported: {sorted(OUTPUT_FORMATS)}" + ) + + driver, extension = OUTPUT_FORMATS[output_format] + + if not os.path.exists(microwatershed_path): + raise FileNotFoundError(f"Microwatershed file not found: {microwatershed_path}") + if not os.path.exists(tehsil_path): + raise FileNotFoundError(f"Tehsil file not found: {tehsil_path}") + + os.makedirs(output_dir, exist_ok=True) + start_time = time.time() + + print("Loading tehsil boundaries...") + tehsil_boundaries = _prepare_tehsil_boundaries( + tehsil_path=tehsil_path, + state=state, + district=district, + tehsil=tehsil, + ) + print(f"Prepared {len(tehsil_boundaries)} unique tehsil boundaries.") + + print("Loading microwatersheds (this can take time once)...") + microwatersheds = _load_microwatersheds( + microwatershed_path=microwatershed_path, + fix_invalid=fix_invalid_mws, + ) + print(f"Loaded {len(microwatersheds)} microwatershed features.") + + if tehsil_boundaries.crs != microwatersheds.crs: + tehsil_boundaries = tehsil_boundaries.to_crs(microwatersheds.crs) + + print("Building microwatershed spatial index...") + mws_sindex = microwatersheds.sindex + + manifest_rows = [] + total = len(tehsil_boundaries) + written_count = 0 + skipped_count = 0 + + for idx, row in enumerate(tehsil_boundaries.itertuples(index=False), start=1): + state_name = row.STATE + district_name = row.District + tehsil_name = row.TEHSIL + tehsil_geom = row.geometry + + try: + candidate_ids = mws_sindex.query(tehsil_geom, predicate="intersects") + except TypeError: + candidate_ids = list(mws_sindex.intersection(tehsil_geom.bounds)) + + if len(candidate_ids) == 0: + intersection_gdf = microwatersheds.iloc[0:0].copy() + else: + candidates = microwatersheds.iloc[list(candidate_ids)] + intersection_gdf = _select_microwatersheds_for_tehsil( + candidates, + tehsil_geom, + clip_to_tehsil=clip_to_tehsil, + ) + + feature_count = len(intersection_gdf) + state_dir = valid_gee_text(str(state_name).strip().lower()) or "unknown_state" + district_dir = ( + valid_gee_text(str(district_name).strip().lower()) or "unknown_district" + ) + tehsil_file = ( + valid_gee_text(str(tehsil_name).strip().lower()) + or f"unknown_tehsil_{idx:05d}" + ) + + output_subdir = os.path.join(output_dir, state_dir, district_dir) + os.makedirs(output_subdir, exist_ok=True) + out_path = os.path.join(output_subdir, f"{tehsil_file}{extension}") + + row_status = "empty" + if feature_count > 0 or include_empty: + if feature_count > 0: + intersection_gdf["STATE"] = state_name + intersection_gdf["District"] = district_name + intersection_gdf["TEHSIL"] = tehsil_name + + if overwrite or not os.path.exists(out_path): + _save_subset(intersection_gdf, out_path, driver) + row_status = "written" + written_count += 1 + else: + row_status = "exists" + skipped_count += 1 + else: + skipped_count += 1 + + manifest_rows.append( + { + "state": state_name, + "district": district_name, + "tehsil": tehsil_name, + "feature_count": feature_count, + "status": row_status, + "output_path": out_path if (feature_count > 0 or include_empty) else "", + } + ) + + if idx % 100 == 0 or idx == total: + print(f"Processed {idx}/{total} tehsils...") + + manifest_path = os.path.join(output_dir, "tehsil_watershed_manifest.csv") + with open(manifest_path, "w", newline="", encoding="utf-8") as csvfile: + writer = csv.DictWriter( + csvfile, + fieldnames=[ + "state", + "district", + "tehsil", + "feature_count", + "status", + "output_path", + ], + ) + writer.writeheader() + writer.writerows(manifest_rows) + + elapsed = time.time() - start_time + print("Done.") + print(f"Output directory: {output_dir}") + print(f"Manifest: {manifest_path}") + print(f"Tehsils processed: {total}") + print(f"Files written: {written_count}") + print(f"Skipped: {skipped_count}") + print(f"Elapsed time: {elapsed:.2f} seconds") + + return manifest_path + + +def _build_parser(): + parser = argparse.ArgumentParser( + description=( + "Precompute and store microwatersheds for each tehsil based on spatial " + "intersection with tehsil boundaries." + ) + ) + parser.add_argument( + "--microwatershed-path", + default=DEFAULT_MICROWATERSHED_PATH, + help="Path to Microwatershed GeoJSON", + ) + parser.add_argument( + "--tehsil-path", + default=DEFAULT_TEHSIL_PATH, + help="Path to tehsil boundary GeoJSON", + ) + parser.add_argument( + "--output-dir", + default=DEFAULT_OUTPUT_DIR, + help="Directory to save per-tehsil watershed files", + ) + parser.add_argument( + "--format", + choices=sorted(OUTPUT_FORMATS.keys()), + default="gpkg", + help="Output file format", + ) + parser.add_argument( + "--overwrite", + action="store_true", + help="Overwrite existing output files", + ) + parser.add_argument( + "--include-empty", + action="store_true", + help="Also write empty files for tehsils with no intersecting microwatersheds", + ) + parser.add_argument( + "--fix-invalid-mws", + action="store_true", + help="Fix invalid microwatershed geometries (slower, use only if needed)", + ) + parser.add_argument( + "--clip-to-tehsil", + action="store_true", + help=( + "Clip exported microwatershed geometries to the tehsil boundary " + "instead of matching GEE filterBounds behavior" + ), + ) + parser.add_argument("--state", help="Optional state filter") + parser.add_argument("--district", help="Optional district filter") + parser.add_argument("--tehsil", help="Optional tehsil filter") + return parser + + +if __name__ == "__main__": + args = _build_parser().parse_args() + generate_tehsil_watershed_copies( + microwatershed_path=args.microwatershed_path, + tehsil_path=args.tehsil_path, + output_dir=args.output_dir, + output_format=args.format, + overwrite=args.overwrite, + include_empty=args.include_empty, + fix_invalid_mws=args.fix_invalid_mws, + clip_to_tehsil=args.clip_to_tehsil, + state=args.state, + district=args.district, + tehsil=args.tehsil, + ) diff --git a/computing/terrain_descriptor/terrain_clusters_local.py b/computing/terrain_descriptor/terrain_clusters_local.py new file mode 100644 index 00000000..ae335477 --- /dev/null +++ b/computing/terrain_descriptor/terrain_clusters_local.py @@ -0,0 +1,182 @@ +import os +import shutil +import tempfile +import zipfile + +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text + +from computing.local_compute_helper import ( + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + build_output_vector_path, + compute_terrain_properties_for_watersheds, + load_precomputed_watersheds, + resolve_clipped_terrain_raster_path, + write_vector_output, +) + + +CLIPPED_TERRAIN_RASTER_DIR = "data/terrain/fabdem_local" +LOCAL_CLUSTER_OUTPUT_DIR = "data/terrain/terrain_clusters_local" +GEOSERVER_WORKSPACE = "terrain" + + +def _push_vector_to_geoserver(gdf, layer_name): + from utilities.geoserver_utils import Geoserver + + temp_dir = tempfile.mkdtemp(prefix=f"{layer_name}_") + gpkg_path = os.path.join(temp_dir, f"{layer_name}.gpkg") + zip_path = os.path.join(temp_dir, f"{layer_name}.zip") + + try: + gdf.to_file(gpkg_path, driver="GPKG", layer=layer_name) + with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf: + zipf.write(gpkg_path, arcname=os.path.basename(gpkg_path)) + + geo = Geoserver() + geo.delete_vector_store(workspace=GEOSERVER_WORKSPACE, store=layer_name) + response = geo.create_shp_datastore( + path=zip_path, + store_name=layer_name, + workspace=GEOSERVER_WORKSPACE, + file_extension="gpkg", + ) + return response + finally: + shutil.rmtree(temp_dir, ignore_errors=True) + + +def run_terrain_clusters_local( + state, + district, + block, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + clipped_raster_dir=CLIPPED_TERRAIN_RASTER_DIR, + push_to_geoserver=True, + sync_layer_metadata=False, +): + state = str(state).strip() + district = str(district).strip() + block = str(block).strip() + + layer_name = ( + f"{valid_gee_text(str(district).strip().lower())}_" + f"{valid_gee_text(str(block).strip().lower())}_cluster" + ) + + watersheds_gdf, watershed_source = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + clipped_raster_path = resolve_clipped_terrain_raster_path( + state=state, + district=district, + block=block, + clipped_raster_dir=clipped_raster_dir, + ) + print(f"Using clipped terrain raster: {clipped_raster_path}") + + terrain_clusters_gdf = compute_terrain_properties_for_watersheds( + watersheds_gdf=watersheds_gdf, + raster_path=clipped_raster_path, + ) + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_CLUSTER_OUTPUT_DIR, + block_fallback="unknown_tehsil", + ) + local_vector_path = write_vector_output( + gdf=terrain_clusters_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local terrain cluster vector: {local_vector_path}") + + if push_to_geoserver: + try: + geoserver_response = _push_vector_to_geoserver( + gdf=terrain_clusters_gdf, + layer_name=layer_name, + ) + print(f"GeoServer response: {geoserver_response}") + except Exception as error: + print(f"Failed to sync terrain clusters vector to GeoServer: {error}") + return False + + if sync_layer_metadata: + from computing.STAC_specs import generate_STAC_layerwise + from computing.utils import save_layer_info_to_db, update_layer_sync_status + + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=local_vector_path, + dataset_name="Terrain Vector", + misc={"is_generated_locally": True}, + algorithm="FABDEM", + algorithm_version="2.0", + ) + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + layer_stac_generated = generate_STAC_layerwise.generate_vector_stac( + state=state, + district=district, + block=block, + layer_name="terrain_vector", + ) + update_layer_sync_status( + layer_id=layer_id, + is_stac_specs_generated=layer_stac_generated, + ) + + print(f"Completed terrain cluster computation for {state}/{district}/{block}") + print(f"Watershed boundary source: {watershed_source}") + return True + + +def _generate_terrain_clusters_task( + state, + district, + block, + gee_account_id=None, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + clipped_raster_dir=CLIPPED_TERRAIN_RASTER_DIR, +): + _ = gee_account_id + return run_terrain_clusters_local( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + clipped_raster_dir=clipped_raster_dir, + push_to_geoserver=True, + sync_layer_metadata=True, + ) + + +@app.task(bind=True) +def generate_terrain_clusters( + self, + state, + district, + block, + gee_account_id=None, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + clipped_raster_dir=CLIPPED_TERRAIN_RASTER_DIR, +): + _ = self + return _generate_terrain_clusters_task( + state=state, + district=district, + block=block, + gee_account_id=gee_account_id, + precomputed_roi_dir=precomputed_roi_dir, + clipped_raster_dir=clipped_raster_dir, + ) diff --git a/computing/terrain_descriptor/terrain_compute_all_local.py b/computing/terrain_descriptor/terrain_compute_all_local.py new file mode 100644 index 00000000..2dedd9aa --- /dev/null +++ b/computing/terrain_descriptor/terrain_compute_all_local.py @@ -0,0 +1,156 @@ +from nrm_app.celery import app + +from computing.lulc_X_terrain.lulc_on_plain_cluster_local import ( + run_lulc_on_plain_cluster_local, +) +from computing.lulc_X_terrain.lulc_on_slope_cluster_local import ( + run_lulc_on_slope_cluster_local, +) +from computing.terrain_descriptor.terrain_clusters_local import ( + run_terrain_clusters_local, +) +from computing.terrain_descriptor.terrain_raster_fabdem_local import ( + run_terrain_raster_fabdem_local, +) + + +def _run_step(step_name, step_func, **kwargs): + print(f"Starting step: {step_name}") + step_result = step_func(**kwargs) + print(f"Completed step: {step_name} -> {step_result}") + if step_result is False: + raise RuntimeError(f"{step_name} failed") + return step_result + + +def _is_missing_block(block): + if block is None: + return True + return str(block).strip().lower() in {"", "null", "none"} + + +def _run_terrain_compute_for_block( + state, + district, + block, + start_year, + end_year, +): + state = str(state).strip() + district = str(district).strip() + block = str(block).strip() + start_year = int(start_year) + end_year = int(end_year) + + results = {} + + results["terrain_raster"] = _run_step( + "terrain_raster", + run_terrain_raster_fabdem_local, + state=state, + district=district, + block=block, + push_to_geoserver=True, + sync_layer_metadata=True, + ) + results["terrain_vector"] = _run_step( + "terrain_vector", + run_terrain_clusters_local, + state=state, + district=district, + block=block, + push_to_geoserver=True, + sync_layer_metadata=True, + ) + results["terrain_lulc_slope"] = _run_step( + "terrain_lulc_slope", + run_lulc_on_slope_cluster_local, + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + push_to_geoserver=True, + sync_layer_metadata=True, + ) + results["terrain_lulc_plain"] = _run_step( + "terrain_lulc_plain", + run_lulc_on_plain_cluster_local, + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + push_to_geoserver=True, + sync_layer_metadata=True, + ) + + print( + f"Completed local terrain compute-all flow for {state}/{district}/{block}: {results}" + ) + return results + + +def run_terrain_compute_all_local( + state, + district, + block, + start_year, + end_year, +): + state = str(state).strip() + district = str(district).strip() + start_year = int(start_year) + end_year = int(end_year) + + if _is_missing_block(block): + raise ValueError( + "block is null. Please provide a block value for terrain compute-all." + ) + + return _run_terrain_compute_for_block( + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + ) + + +def _generate_terrain_compute_all_local_task( + state, + district, + block, + start_year, + end_year, + gee_account_id=None, +): + _ = gee_account_id + return run_terrain_compute_all_local( + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + ) + + +@app.task(bind=True) +def generate_terrain_compute_all( + self, + state, + district, + block, + start_year, + end_year, + gee_account_id=None, +): + _ = self + return _generate_terrain_compute_all_local_task( + state=state, + district=district, + block=block, + start_year=start_year, + end_year=end_year, + gee_account_id=gee_account_id, + ) diff --git a/computing/terrain_descriptor/terrain_raster_fabdem_local.py b/computing/terrain_descriptor/terrain_raster_fabdem_local.py new file mode 100644 index 00000000..f8f9bd1f --- /dev/null +++ b/computing/terrain_descriptor/terrain_raster_fabdem_local.py @@ -0,0 +1,167 @@ +from utilities.gee_utils import valid_gee_text + +from nrm_app.celery import app + +from computing.local_compute_helper import ( + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + TERRAIN_RASTER_PATH, + build_output_raster_path, + clip_raster_with_roi, + load_precomputed_roi, + push_local_raster_to_geoserver, + read_validated_vector_file, +) + + +LOCAL_OUTPUT_BASE_DIR = "data/terrain/fabdem_local" +GEOSERVER_STYLE = "terrain_raster" +GEOSERVER_WORKSPACE = "terrain" + + +def run_terrain_raster_fabdem_local( + state=None, + district=None, + block=None, + asset_suffix=None, + roi=None, + precomputed_roi_dir=None, + push_to_geoserver=True, + sync_layer_metadata=False, +): + if state and district and block: + layer_name = ( + f"{valid_gee_text(str(district).strip().lower())}_" + f"{valid_gee_text(str(block).strip().lower())}_terrain_raster" + ) + roi_gdf = load_precomputed_roi( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + else: + if not roi or not asset_suffix: + raise ValueError( + "For non state/district/block runs, both `roi` and `asset_suffix` are required." + ) + layer_name = f"{asset_suffix}_terrain_raster".lower() + roi_gdf = read_validated_vector_file( + roi, + f"ROI file has no valid geometries: {roi}", + ) + + output_raster_path = build_output_raster_path( + layer_name=layer_name, + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + state=state, + district=district, + block=block, + ) + clipped_raster_path = clip_raster_with_roi( + roi_gdf=roi_gdf, + raster_path=TERRAIN_RASTER_PATH, + output_path=output_raster_path, + ) + + print(f"Local clipped FABDEM raster written to: {clipped_raster_path}") + + if push_to_geoserver: + try: + upload_res, style_res = push_local_raster_to_geoserver( + file_path=clipped_raster_path, + layer_name=layer_name, + workspace=GEOSERVER_WORKSPACE, + style_name=GEOSERVER_STYLE, + ) + print(f"GeoServer upload response: {upload_res}") + print(f"GeoServer style response: {style_res}") + except Exception as error: + print(f"Failed to sync local FABDEM raster to GeoServer: {error}") + return False + + if sync_layer_metadata and state and district and block: + from computing.STAC_specs import generate_STAC_layerwise + from computing.utils import save_layer_info_to_db, update_layer_sync_status + + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=clipped_raster_path, + dataset_name="Terrain Raster", + misc={"is_generated_locally": True}, + algorithm="FABDEM", + algorithm_version="2.0", + ) + + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("sync to geoserver flag is updated") + + layer_stac_generated = generate_STAC_layerwise.generate_raster_stac( + state=state, + district=district, + block=block, + layer_name="terrain_raster", + ) + update_layer_sync_status( + layer_id=layer_id, + is_stac_specs_generated=layer_stac_generated, + ) + + return True + + +def _generate_terrain_raster_clip_task( + state=None, + district=None, + block=None, + gee_account_id=None, + asset_suffix=None, + asset_folder=None, + proj_id=None, + roi=None, + precomputed_roi_dir=None, + app_type="MWS", +): + _ = gee_account_id, asset_folder, proj_id, app_type + return run_terrain_raster_fabdem_local( + state=state, + district=district, + block=block, + asset_suffix=asset_suffix, + roi=roi, + precomputed_roi_dir=precomputed_roi_dir, + push_to_geoserver=True, + sync_layer_metadata=True, + ) + + +@app.task(bind=True) +def generate_terrain_raster_clip( + self, + state=None, + district=None, + block=None, + gee_account_id=None, + asset_suffix=None, + asset_folder=None, + proj_id=None, + roi=None, + precomputed_roi_dir=None, + app_type="MWS", +): + _ = self + return _generate_terrain_raster_clip_task( + state=state, + district=district, + block=block, + gee_account_id=gee_account_id, + asset_suffix=asset_suffix, + asset_folder=asset_folder, + proj_id=proj_id, + roi=roi, + precomputed_roi_dir=precomputed_roi_dir, + app_type=app_type, + ) diff --git a/computing/test_base_layer_setup.py b/computing/test_base_layer_setup.py new file mode 100644 index 00000000..0d315c3a --- /dev/null +++ b/computing/test_base_layer_setup.py @@ -0,0 +1,108 @@ +import tempfile +from pathlib import Path +from unittest.mock import Mock, patch + +from django.test import SimpleTestCase + +from computing.base_layer_setup import ( + _download_active_tehsil_watersheds, + with_tehsil_watershed, +) + + +class GeoServerTehsilWatershedSetupTests(SimpleTestCase): + def setUp(self): + self.temp_dir = tempfile.TemporaryDirectory() + self.output_dir = Path(self.temp_dir.name) + self.location = [("Bihar", "Banka", "Banka")] + + def tearDown(self): + self.temp_dir.cleanup() + + @patch("computing.base_layer_setup.requests.get") + @patch("computing.base_layer_setup._active_tehsil_locations") + def test_existing_active_tehsil_is_skipped(self, active_locations, get): + active_locations.return_value = self.location + destination = self.output_dir / "bihar" / "banka" / "banka.gpkg" + destination.parent.mkdir(parents=True) + destination.write_bytes(b"existing") + + with patch( + "computing.base_layer_setup.TEHSIL_WATERSHEDS_DIR", + self.output_dir, + ): + _download_active_tehsil_watersheds() + + get.assert_not_called() + self.assertEqual(destination.read_bytes(), b"existing") + + @patch("geopandas.GeoDataFrame.from_features") + @patch("computing.base_layer_setup.requests.get") + @patch("computing.base_layer_setup._active_tehsil_locations") + def test_force_replaces_active_tehsil_gpkg( + self, + active_locations, + get, + from_features, + ): + active_locations.return_value = self.location + response = Mock() + response.json.return_value = { + "type": "FeatureCollection", + "features": [{"type": "Feature", "properties": {}, "geometry": None}], + } + get.return_value = response + + watersheds = Mock() + watersheds.empty = False + watersheds.to_file.side_effect = lambda path, **kwargs: Path(path).write_bytes( + b"replacement" + ) + from_features.return_value = watersheds + + destination = self.output_dir / "bihar" / "banka" / "banka.gpkg" + destination.parent.mkdir(parents=True) + destination.write_bytes(b"existing") + + with patch( + "computing.base_layer_setup.TEHSIL_WATERSHEDS_DIR", + self.output_dir, + ): + _download_active_tehsil_watersheds(force=True) + + self.assertEqual(destination.read_bytes(), b"replacement") + get.assert_called_once() + self.assertEqual( + get.call_args.kwargs["params"]["typeName"], + "mws:mws_banka_banka", + ) + watersheds.to_file.assert_called_once_with( + destination.with_suffix(".tmp.gpkg"), + layer="watersheds", + driver="GPKG", + ) + + @patch("computing.base_layer_setup.ensure_tehsil_watershed") + def test_local_compute_ensures_requested_tehsil(self, ensure): + @with_tehsil_watershed + def generate(state, district, block, compute="gee"): + return "generated" + + result = generate("Bihar", "Banka", "Banka", compute="local") + + self.assertEqual(result, "generated") + ensure.assert_called_once_with( + state="Bihar", + district="Banka", + tehsil="Banka", + ) + + @patch("computing.base_layer_setup.ensure_tehsil_watershed") + def test_gee_compute_does_not_ensure_local_tehsil(self, ensure): + @with_tehsil_watershed + def generate(state, district, block, compute="gee"): + return "generated" + + generate("Bihar", "Banka", "Banka") + + ensure.assert_not_called() diff --git a/computing/tests.py b/computing/tests.py index 7ce503c2..cd08ff26 100644 --- a/computing/tests.py +++ b/computing/tests.py @@ -1,3 +1,722 @@ -from django.test import TestCase +from io import StringIO +from inspect import signature +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import MagicMock, call, patch -# Create your tests here. +import geopandas as gpd +import pandas as pd +from django.core.management import call_command +from django.core.management.base import CommandError +from django.test import SimpleTestCase, TestCase, override_settings +from shapely.geometry import Point + +from computing.bulk_layer_generation import ( + Location, + get_active_locations, + get_active_locations_from_api, + get_locally_generated_locations, + get_regeneration_dataset_names, + pipeline_names, + run_pipeline, +) +from computing.layer_dependency.layer_generation_in_order import get_args, load_map_config +from computing.local_compute_helper import write_vector_output +from computing.misc.nrega_local_compute import _compute_nrega_for_watersheds +from computing.models import Dataset, Layer +from computing.surface_water_bodies.swb_local import ( + _continue_swb_in_gee, + _convert_area_columns_to_hectares, + _final_layer_name, + _layer_name, + run_swb_local, +) +from computing.surface_water_bodies.swb3 import ( + waterbody_catchment_streamorder_properties, +) +from computing.utils import generate_swb_layer_with_max_so_catchment +from computing.tasks import bulk_generate_layer +from geoadmin.models import DistrictSOI, StateSOI, TehsilSOI +from utilities.constants import ( + CATCHMENT_AREA, + PAN_INDIA_DRAINAGE_LINES_DATASET, + STREAM_ORDER_ASSET, +) + + +class LocalNregaTests(SimpleTestCase): + def test_timestamp_attributes_can_be_written_to_geopackage(self): + watersheds = gpd.GeoDataFrame( + geometry=[Point(0, 0)], + crs="EPSG:4326", + ) + nrega = gpd.GeoDataFrame( + { + "work_start": pd.to_datetime(["2026-08-05"]), + "geometry": [Point(0, 0)], + }, + crs="EPSG:4326", + ) + + result = _compute_nrega_for_watersheds(watersheds, nrega) + + self.assertEqual(result.loc[0, "work_start"], "2026-08-05T00:00:00") + with TemporaryDirectory() as output_dir: + output_path = Path(output_dir) / "nrega.gpkg" + write_vector_output(result, output_path, "nrega") + self.assertTrue(output_path.exists()) + + +class LocalSwbContinuationTests(SimpleTestCase): + @patch("computing.surface_water_bodies.swb_local._complete_swb_pipeline") + @patch("computing.surface_water_bodies.swb_local._push_local_swb_to_geoserver") + @patch("computing.surface_water_bodies.swb_local.make_asset_public") + @patch("computing.surface_water_bodies.swb_local.is_gee_asset_exists") + @patch("computing.surface_water_bodies.swb_local._create_local_swb_output") + @patch("computing.surface_water_bodies.swb_local.build_output_vector_path") + @patch("computing.surface_water_bodies.swb_local._resolve_gee_asset_id") + @patch("computing.surface_water_bodies.swb_local.gdf_to_ee_fc") + @patch("computing.surface_water_bodies.swb_local.ee_initialize") + @patch("computing.surface_water_bodies.swb_local._union_geometry") + @patch("computing.surface_water_bodies.swb_local._resolve_roi_gdf") + def test_existing_swb2_asset_recreates_and_syncs_missing_local_output( + self, + resolve_roi, + union_geometry, + ee_initialize, + gdf_to_ee_fc, + resolve_asset, + build_output_path, + create_local_output, + asset_exists, + make_public, + push_swb2, + complete_pipeline, + ): + roi_gdf = MagicMock() + roi_gdf.__len__.return_value = 1 + resolve_roi.return_value = roi_gdf + union_geometry.return_value.is_empty = False + gdf_to_ee_fc.return_value = MagicMock() + resolve_asset.return_value = ( + "swb2_bid_bid_local", + "projects/example/swb2_bid_bid_local", + ["maharashtra", "bid", "bid"], + ) + output_path = Path("/tmp/nonexistent-swb2-output.gpkg") + build_output_path.return_value = output_path + create_local_output.return_value = (MagicMock(), str(output_path)) + asset_exists.return_value = True + events = [] + push_swb2.side_effect = lambda **kwargs: events.append("swb2") or True + complete_pipeline.side_effect = ( + lambda **kwargs: events.append("complete") or True + ) + + result = run_swb_local( + state="Maharashtra", + district="Bid", + block="Bid", + sync_layer_metadata=False, + ) + + create_local_output.assert_called_once() + push_swb2.assert_called_once_with( + output_path=str(output_path), + layer_name="surface_waterbodies_bid_bid", + ) + complete_pipeline.assert_called_once() + self.assertEqual(events, ["swb2", "complete"]) + self.assertTrue(result) + + def test_reconstructs_area_ored_before_converting_annual_areas(self): + source = pd.DataFrame( + { + "area_2020": [500, 0], + "k_2020": [83.3333, 0], + "area_2021": [600, 240], + "k_2021": [100, 20], + "total_area_m2": [550, 1200], + } + ) + + converted, _ = _convert_area_columns_to_hectares(source) + + self.assertAlmostEqual(converted.loc[0, "area_ored"], 0.06, places=6) + self.assertAlmostEqual(converted.loc[1, "area_ored"], 0.12, places=6) + self.assertEqual(converted["area_2020"].tolist(), [0.05, 0]) + self.assertEqual(converted["total_area"].tolist(), [0.055, 0.12]) + + def test_preserves_existing_area_ored(self): + source = pd.DataFrame( + { + "area_ored": [0.25], + "area_2025": [500], + "k_2025": [100], + } + ) + + converted, _ = _convert_area_columns_to_hectares(source) + + self.assertEqual(converted.loc[0, "area_ored"], 0.25) + + def test_swb_enrichment_defaults_to_pan_india_assets(self): + raster_parameters = signature( + generate_swb_layer_with_max_so_catchment + ).parameters + swb3_parameters = signature( + waterbody_catchment_streamorder_properties + ).parameters + + self.assertEqual( + raster_parameters["stream_order_asset_id"].default, + STREAM_ORDER_ASSET, + ) + self.assertEqual( + raster_parameters["catchment_area_asset_id"].default, + CATCHMENT_AREA, + ) + self.assertEqual( + swb3_parameters["drainage_lines_asset_id"].default, + PAN_INDIA_DRAINAGE_LINES_DATASET, + ) + + def test_geoserver_layers_have_no_local_suffix(self): + expected_layer_name = "surface_waterbodies_dumka_jarmundi" + self.assertEqual( + _layer_name("dumka_jarmundi"), + expected_layer_name, + ) + self.assertEqual( + _final_layer_name("dumka_jarmundi"), + expected_layer_name, + ) + + def test_local_map_passes_gee_account_to_swb(self): + args = get_args( + iterator_name={ + "name": "generate_swb", + "use_global_args": True, + "pass_gee_account_id": True, + }, + global_args={"start_year": 2017, "end_year": 2024}, + gee_account_id="22", + compute="local", + ) + + self.assertEqual( + args, + { + "start_year": 2017, + "end_year": 2024, + "gee_account_id": "22", + }, + ) + + def test_local_map_runs_swb_without_generated_gee_dependencies(self): + swb_node = next( + node + for node in load_map_config("dynamic_layers", compute="local") + if node["name"] == "generate_swb" + ) + + self.assertTrue(swb_node["pass_gee_account_id"]) + + @patch("computing.surface_water_bodies.swb_local.make_asset_public") + @patch("computing.surface_water_bodies.swb_local.is_gee_asset_exists") + @patch("computing.surface_water_bodies.swb_local.check_task_status") + @patch("computing.surface_water_bodies.swb_local.waterbody_wbc_intersection") + @patch( + "computing.surface_water_bodies.swb_local.waterbody_catchment_streamorder_properties" + ) + def test_runs_swb3_and_swb4_without_local_suffix( + self, + generate_swb3, + generate_swb4, + check_task_status, + is_gee_asset_exists, + make_asset_public, + ): + generate_swb3.return_value = ("swb3-task", "swb3-asset") + generate_swb4.return_value = ("swb4-task", "swb4-asset") + is_gee_asset_exists.return_value = True + roi = object() + + result = _continue_swb_in_gee( + state="odisha", + asset_suffix="district_block", + asset_folder_list=["odisha", "district", "block"], + app_type="MWS", + gee_account_id="account", + roi=roi, + ) + + generate_swb3.assert_called_once_with( + roi=roi, + asset_suffix="district_block", + asset_folder_list=["odisha", "district", "block"], + app_type="MWS", + gee_account_id="account", + swb2_asset_suffix="district_block_local", + ) + generate_swb4.assert_called_once_with( + roi=roi, + state="odisha", + asset_suffix="district_block", + asset_folder_list=["odisha", "district", "block"], + app_type="MWS", + ) + self.assertEqual( + check_task_status.call_args_list, + [call(["swb3-task"]), call(["swb4-task"])], + ) + self.assertEqual( + make_asset_public.call_args_list, + [call("swb3-asset"), call("swb4-asset")], + ) + self.assertEqual(result, ("district_block", "swb3-asset")) + + +class BulkPipelineRegistryTests(SimpleTestCase): + def test_all_local_pipelines_define_regeneration_datasets(self): + for pipeline in pipeline_names("local"): + self.assertTrue( + get_regeneration_dataset_names(pipeline), + pipeline, + ) + + @patch("computing.bulk_layer_generation.import_string") + def test_registered_pipeline_builds_standard_payload(self, import_string): + runner = import_string.return_value + location = { + "state": "Jharkhand", + "district": "Dumka", + "block": "Masalia", + } + + run_pipeline("antyodaya", location, overwrite=False) + + runner.assert_called_once_with( + { + "scope": { + "level": "tehsil", + "state_name": "Jharkhand", + "district_name": "Dumka", + "tehsil_name": "Masalia", + }, + "outputs": {}, + "publish": { + "sync_to_geoserver": True, + "overwrite": False, + "register_layers": True, + "use_pregenerated": False, + }, + } + ) + + @patch("computing.tasks.run_pipeline") + def test_bulk_task_runs_registered_pipeline(self, run_registered_pipeline): + location = { + "state": "Jharkhand", + "district": "Dumka", + "block": "Masalia", + } + + bulk_generate_layer.run("livestocks", location, overwrite=False) + + run_registered_pipeline.assert_called_once_with( + "livestocks", + location, + overwrite=False, + compute="local", + start_year=None, + end_year=None, + gee_account_id=None, + ) + + @patch("computing.bulk_layer_generation._task_registry") + def test_existing_registry_pipeline_receives_supported_arguments( + self, task_registry + ): + def lulc( + state, + district, + block, + start_year, + end_year, + gee_account_id=None, + ): + return ( + state, + district, + block, + start_year, + end_year, + gee_account_id, + ) + + task_registry.return_value = {"lulc_v3": lulc} + + result = run_pipeline( + "lulc_v3", + { + "state": "Jharkhand", + "district": "Dumka", + "block": "Masalia", + }, + compute="local", + start_year=2018, + end_year=2024, + ) + + self.assertEqual( + result, + ("Jharkhand", "Dumka", "Masalia", 2018, 2024, None), + ) + + +class BulkLayerCommandTests(SimpleTestCase): + def test_command_requires_explicit_scope(self): + with self.assertRaisesMessage(CommandError, "Specify --all-active"): + call_command("bulk_generate_layers", "antyodaya") + + @patch("computing.bulk_layer_generation._task_registry", return_value={}) + def test_command_rejects_unknown_pipeline(self, task_registry): + with self.assertRaisesMessage(CommandError, "Unknown local pipeline"): + call_command("bulk_generate_layers", "missing", "--all-active") + + @patch( + "computing.management.commands.bulk_generate_layers." + "bulk_generate_layer.apply_async" + ) + @patch( + "computing.management.commands.bulk_generate_layers.get_active_locations" + ) + def test_command_dry_run_does_not_enqueue( + self, get_locations, apply_async + ): + get_locations.return_value = [ + Location("Jharkhand", "Dumka", "Jarmundi") + ] + output = StringIO() + + call_command( + "bulk_generate_layers", + "antyodaya", + "--all-active", + "--limit=1", + "--dry-run", + stdout=output, + ) + + apply_async.assert_not_called() + self.assertIn("Dry run complete", output.getvalue()) + + @patch( + "computing.management.commands.bulk_generate_layers." + "bulk_generate_layer.apply_async" + ) + @patch( + "computing.management.commands.bulk_generate_layers.get_active_locations" + ) + def test_command_routes_tasks_to_bulk_queue( + self, get_locations, apply_async + ): + get_locations.return_value = [ + Location("Jharkhand", "Dumka", "Masalia") + ] + apply_async.return_value.id = "task-id" + + call_command( + "bulk_generate_layers", + "livestocks", + "--block=Masalia", + "--no-overwrite", + stdout=StringIO(), + ) + + apply_async.assert_called_once_with( + kwargs={ + "pipeline": "livestocks", + "location": { + "state": "Jharkhand", + "district": "Dumka", + "block": "Masalia", + }, + "overwrite": False, + "compute": "local", + "start_year": None, + "end_year": None, + "gee_account_id": None, + }, + queue="layer_bulk", + ) + get_locations.assert_called_once_with( + blocks=["Masalia"], + limit=None, + ) + + @patch( + "computing.management.commands.bulk_generate_layers." + "bulk_generate_layer.apply_async" + ) + @patch( + "computing.management.commands.bulk_generate_layers.get_active_locations" + ) + def test_command_accepts_multiple_blocks(self, get_locations, apply_async): + get_locations.return_value = [ + Location("Jharkhand", "Dumka", "Jarmundi"), + Location("Jharkhand", "Dumka", "Masalia"), + ] + apply_async.return_value.id = "task-id" + + call_command( + "bulk_generate_layers", + "lulc_v3", + "--district=Dumka", + "--block=Jarmundi", + "--block=Masalia", + "--start-year=2018", + "--end-year=2024", + stdout=StringIO(), + ) + + get_locations.assert_called_once_with( + district="Dumka", + blocks=["Jarmundi", "Masalia"], + limit=None, + ) + self.assertEqual(apply_async.call_count, 2) + + @patch( + "computing.management.commands.bulk_generate_layers." + "get_active_locations_from_api" + ) + def test_command_loads_locations_from_prod_api(self, get_locations): + get_locations.return_value = [ + Location("Jharkhand", "Dumka", "Masalia") + ] + + call_command( + "bulk_generate_layers", + "livestocks", + "--all-active", + "--from-prod-api", + "--dry-run", + stdout=StringIO(), + ) + + get_locations.assert_called_once_with(limit=None) + + @patch( + "computing.management.commands.bulk_generate_layers." + "bulk_generate_layer.apply_async" + ) + @patch( + "computing.management.commands.bulk_generate_layers." + "get_locally_generated_locations" + ) + def test_command_regenerates_locally_generated_locations( + self, get_locations, apply_async + ): + get_locations.return_value = [ + Location("Jharkhand", "Dumka", "Masalia") + ] + apply_async.return_value.id = "task-id" + + call_command( + "bulk_generate_layers", + "livestocks", + "--regenerate-local", + "--district=Dumka", + stdout=StringIO(), + ) + + get_locations.assert_called_once_with( + dataset_names=("Livestock Census 2019",), + district="Dumka", + limit=None, + ) + apply_async.assert_called_once() + + def test_change_detection_regeneration_uses_vector_dataset(self): + self.assertEqual( + get_regeneration_dataset_names("change_detection_vector"), + ("Change Detection Vector",), + ) + + +@override_settings(PROD_BACKEND_URL="https://geoserver.core-stack.org/") +class ActiveLocationsApiTests(SimpleTestCase): + @patch("computing.bulk_layer_generation.requests.get") + def test_filters_and_limits_api_locations(self, get): + get.return_value.json.return_value = [ + { + "label": "Jharkhand", + "district": [ + { + "label": "Dumka", + "blocks": [ + {"label": "Jarmundi"}, + {"label": "Masalia"}, + ], + } + ], + }, + { + "label": "Odisha", + "district": [ + { + "label": "Mayurbhanj", + "blocks": [{"label": "Baripada"}], + } + ], + }, + ] + + locations = get_active_locations_from_api( + state="jharkhand", + district="dumka", + blocks=["MASALIA", "jarmundi"], + limit=1, + ) + + self.assertEqual( + locations, + [Location("Jharkhand", "Dumka", "Jarmundi")], + ) + get.assert_called_once_with( + "https://geoserver.core-stack.org/api/v1/proposed_blocks/", + timeout=30, + ) + get.return_value.raise_for_status.assert_called_once_with() + + @override_settings(PROD_BACKEND_URL="") + def test_requires_prod_backend_url(self): + with self.assertRaisesMessage( + ValueError, "PROD_BACKEND_URL is not configured" + ): + get_active_locations_from_api() + + +class BulkLayerGenerationTests(TestCase): + @classmethod + def setUpTestData(cls): + active_state = StateSOI.objects.create( + state_name="Jharkhand", active_status=True + ) + inactive_state = StateSOI.objects.create( + state_name="Odisha", active_status=False + ) + dumka = DistrictSOI.objects.create( + state=active_state, + district_name="Dumka", + active_status=True, + ) + inactive_district = DistrictSOI.objects.create( + state=active_state, + district_name="Inactive District", + active_status=False, + ) + inactive_state_district = DistrictSOI.objects.create( + state=inactive_state, + district_name="Mayurbhanj", + active_status=True, + ) + TehsilSOI.objects.create( + district=dumka, tehsil_name="Masalia", active_status=True + ) + TehsilSOI.objects.create( + district=dumka, tehsil_name="Jarmundi", active_status=True + ) + TehsilSOI.objects.create( + district=dumka, tehsil_name="Inactive Block", active_status=False + ) + TehsilSOI.objects.create( + district=inactive_district, + tehsil_name="Active Block", + active_status=True, + ) + TehsilSOI.objects.create( + district=inactive_state_district, + tehsil_name="Baripada", + active_status=True, + ) + + def test_active_locations_require_active_hierarchy_and_are_ordered(self): + locations = get_active_locations() + + self.assertEqual( + [location.block for location in locations], + ["Jarmundi", "Masalia"], + ) + + def test_active_locations_apply_case_insensitive_filters_and_limit(self): + locations = get_active_locations( + state="jharkhand", + district="dumka", + block="jarmundi", + limit=1, + ) + + self.assertEqual(len(locations), 1) + self.assertEqual( + locations[0].asdict(), + { + "state": "Jharkhand", + "district": "Dumka", + "block": "Jarmundi", + }, + ) + + def test_active_locations_accept_multiple_blocks(self): + locations = get_active_locations( + state="jharkhand", + district="dumka", + blocks=["masalia", "JARMUNDI"], + ) + + self.assertEqual( + [location.block for location in locations], + ["Jarmundi", "Masalia"], + ) + + def test_locally_generated_locations_are_distinct_and_filterable(self): + dataset = Dataset.objects.create(name="Local Dataset") + other_dataset = Dataset.objects.create(name="Other Local Dataset") + jarmundi = TehsilSOI.objects.get(tehsil_name="Jarmundi") + masalia = TehsilSOI.objects.get(tehsil_name="Masalia") + for layer_name in ("local_one", "local_two"): + Layer.objects.create( + dataset=dataset, + layer_name=layer_name, + state=jarmundi.district.state, + district=jarmundi.district, + block=jarmundi, + misc={"is_generated_locally": True}, + ) + Layer.objects.create( + dataset=other_dataset, + layer_name="other_local_layer", + state=masalia.district.state, + district=masalia.district, + block=masalia, + misc={"is_generated_locally": True}, + ) + + locations = get_locally_generated_locations( + dataset_names=("Local Dataset",), + district="dumka", + blocks=["JARMUNDI", "Masalia"], + ) + + self.assertEqual( + [location.asdict() for location in locations], + [ + { + "state": "Jharkhand", + "district": "Dumka", + "block": "Jarmundi", + } + ], + ) diff --git a/computing/tree_health/canopy_height.py b/computing/tree_health/gee/canopy_height.py similarity index 90% rename from computing/tree_health/canopy_height.py rename to computing/tree_health/gee/canopy_height.py index 9877d6df..c7d804ff 100644 --- a/computing/tree_health/canopy_height.py +++ b/computing/tree_health/gee/canopy_height.py @@ -14,6 +14,7 @@ get_gee_dir_path, ) from computing.utils import save_layer_info_to_db, update_layer_sync_status +from computing.STAC_specs import generate_STAC_layerwise # Celery task to generate canopy height raster @@ -132,6 +133,17 @@ def tree_health_ch_raster( if res and layer_id: layer_at_geoserver = True + + # layer_STAC_generated = False + # layer_STAC_generated = generate_STAC_layerwise.generate_raster_stac( + # state=state, + # district=district, + # block=block, + # layer_name="ch_raster", + # start_year=year, + # ) + + # Update sync flag in DB update_layer_sync_status( layer_id=layer_id, sync_to_geoserver=layer_at_geoserver, diff --git a/computing/tree_health/canopy_height_vector.py b/computing/tree_health/gee/canopy_height_vector.py similarity index 99% rename from computing/tree_health/canopy_height_vector.py rename to computing/tree_health/gee/canopy_height_vector.py index 57ca2db5..f8b5785a 100644 --- a/computing/tree_health/canopy_height_vector.py +++ b/computing/tree_health/gee/canopy_height_vector.py @@ -100,7 +100,7 @@ def tree_health_ch_vector( merged_fc = ee.FeatureCollection(asset_id) # Sync to GeoServer - sync_res = sync_fc_to_geoserver(merged_fc, state, description, "canopy_height_vector") + sync_res = sync_fc_to_geoserver(merged_fc, state, description, "tree_ch_vector") # Save layer info if asset exists if is_gee_asset_exists(asset_id): diff --git a/computing/tree_health/ccd.py b/computing/tree_health/gee/ccd.py similarity index 89% rename from computing/tree_health/ccd.py rename to computing/tree_health/gee/ccd.py index e358ff47..f3155e0f 100644 --- a/computing/tree_health/ccd.py +++ b/computing/tree_health/gee/ccd.py @@ -14,6 +14,7 @@ get_gee_dir_path, ) from computing.utils import save_layer_info_to_db, update_layer_sync_status +from computing.STAC_specs import generate_STAC_layerwise # Celery task to generate CCD raster @@ -133,6 +134,17 @@ def tree_health_ccd_raster( if res and layer_id: layer_at_geoserver = True + + # layer_STAC_generated = False + # layer_STAC_generated = generate_STAC_layerwise.generate_raster_stac( + # state=state, + # district=district, + # block=block, + # layer_name="ccd_raster", + # start_year=year, + # ) + + # Update sync status in DB update_layer_sync_status( layer_id=layer_id, sync_to_geoserver=layer_at_geoserver, diff --git a/computing/tree_health/ccd_vector.py b/computing/tree_health/gee/ccd_vector.py similarity index 100% rename from computing/tree_health/ccd_vector.py rename to computing/tree_health/gee/ccd_vector.py diff --git a/computing/tree_health/overall_change.py b/computing/tree_health/gee/overall_change.py similarity index 93% rename from computing/tree_health/overall_change.py rename to computing/tree_health/gee/overall_change.py index 2209d561..a3c3c21a 100644 --- a/computing/tree_health/overall_change.py +++ b/computing/tree_health/gee/overall_change.py @@ -14,6 +14,7 @@ get_gee_dir_path, ) from computing.utils import save_layer_info_to_db, update_layer_sync_status +from computing.STAC_specs import generate_STAC_layerwise @app.task(bind=True) @@ -110,6 +111,13 @@ def tree_health_overall_change_raster( if res and layer_id: layer_at_geoserver = True + # layer_STAC_generated = False + # layer_STAC_generated = generate_STAC_layerwise.generate_raster_stac( + # state=state, + # district=district, + # block=block, + # layer_name="tree_cover_change_raster", + # ) update_layer_sync_status( layer_id=layer_id, sync_to_geoserver=layer_at_geoserver, diff --git a/computing/tree_health/overall_change_vector.py b/computing/tree_health/gee/overall_change_vector.py similarity index 100% rename from computing/tree_health/overall_change_vector.py rename to computing/tree_health/gee/overall_change_vector.py diff --git a/computing/tree_health/local/canopy_height_local.py b/computing/tree_health/local/canopy_height_local.py new file mode 100644 index 00000000..5032701a --- /dev/null +++ b/computing/tree_health/local/canopy_height_local.py @@ -0,0 +1,278 @@ +from pathlib import Path + +import numpy as np +import rasterio +from rasterio.mask import mask +from rasterio.warp import Resampling, reproject +from shapely.geometry import mapping + +from computing.config_loader import LULC_BASE_DIR, PROJECT_ROOT +from computing.local_compute_helper import ( + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + build_output_raster_path, + get_union_geometry, + load_precomputed_roi, + push_local_raster_to_geoserver, + read_validated_vector_file, + resolve_lulc_raster_paths, + validate_geometry, +) +from computing.utils import save_layer_info_to_db, update_layer_sync_status +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text + + +LOCAL_CH_BASE_DIR = PROJECT_ROOT / "data/base_layers/tree_health/ch" +LOCAL_OUTPUT_BASE_DIR = PROJECT_ROOT / "data/tree_health" +GEOSERVER_WORKSPACE = "tree_ch_raster" +GEOSERVER_STYLE = "tree_ch_style" + +# LULC class 6 is tree cover. CH values are retained only on tree pixels. +TREE_LULC_CLASS = 6 + + +def _slug(value, fallback): + if value is None: + return fallback + return valid_gee_text(str(value).strip().lower()) or fallback + + +def _resolve_ch_raster(year, ch_dir=LOCAL_CH_BASE_DIR): + # Local canopy height files are expected to be pre-downloaded. + ch_dir = Path(ch_dir) + possible_names = [ + f"CH_raster_{year}.tif", + f"ch_raster_{year}.tif", + f"canopy_height_{year}.tif", + f"{year}.tif", + ] + + for name in possible_names: + path = ch_dir / name + if path.exists(): + return str(path) + + matches = sorted(ch_dir.glob(f"*{year}*.tif")) + if matches: + return str(matches[0]) + + raise FileNotFoundError( + f"Local canopy height raster for {year} not found in {ch_dir}." + ) + + +def _pick_output_nodata(dtype, source_nodata): + # CH class values are 0, 1, 2 and 3, so avoid using those as nodata. + if source_nodata is not None: + source_nodata = float(source_nodata) + if not np.isnan(source_nodata) and source_nodata not in (0.0, 1.0, 2.0, 3.0): + return source_nodata + + dtype = np.dtype(dtype) + if np.issubdtype(dtype, np.floating): + return -9999.0 + + info = np.iinfo(dtype) + if np.issubdtype(dtype, np.signedinteger): + return info.min + return info.max + + +def _clip_and_mask_ch(ch_path, lulc_path, roi_gdf, output_path): + with rasterio.open(ch_path) as ch_src: + # Align ROI with the canopy height raster before clipping. + roi_gdf = validate_geometry(roi_gdf) + if roi_gdf.empty: + raise ValueError("No valid ROI geometry available for local CH clipping.") + if roi_gdf.crs is None: + raise ValueError("ROI CRS is missing; cannot align canopy height raster.") + if ch_src.crs and roi_gdf.crs != ch_src.crs: + roi_gdf = roi_gdf.to_crs(ch_src.crs) + + roi_union = get_union_geometry(roi_gdf) + if roi_union is None or roi_union.is_empty: + raise ValueError("ROI union geometry is empty for local CH clipping.") + + # Prefer the class band if the source raster has band descriptions. + band_index = 1 + for index, description in enumerate(ch_src.descriptions, start=1): + if description and description.strip().lower() in ("ch_class", "ch"): + band_index = index + break + + nodata = _pick_output_nodata( + dtype=ch_src.dtypes[band_index - 1], + source_nodata=ch_src.nodata, + ) + ch_array, ch_transform = mask( + ch_src, + shapes=[mapping(roi_union)], + crop=True, + filled=True, + nodata=nodata, + indexes=band_index, + ) + if ch_array.ndim == 3: + ch_array = ch_array[0] + + output_meta = ch_src.meta.copy() + output_meta.update( + { + "driver": "GTiff", + "height": ch_array.shape[0], + "width": ch_array.shape[1], + "transform": ch_transform, + "count": 1, + "dtype": ch_array.dtype, + "nodata": nodata, + "compress": "lzw", + } + ) + + # Reproject LULC to the clipped CH grid and use it as the tree mask. + lulc_array = np.zeros((output_meta["height"], output_meta["width"]), dtype=np.uint8) + with rasterio.open(lulc_path) as lulc_src: + reproject( + source=rasterio.band(lulc_src, 1), + destination=lulc_array, + src_transform=lulc_src.transform, + src_crs=lulc_src.crs, + src_nodata=lulc_src.nodata, + dst_transform=output_meta["transform"], + dst_crs=output_meta["crs"], + dst_nodata=0, + resampling=Resampling.mode, + ) + + tree_mask = lulc_array == TREE_LULC_CLASS + valid_ch = ch_array != nodata + output_array = np.where(tree_mask & valid_ch, ch_array, nodata).astype( + ch_array.dtype, + copy=False, + ) + + with rasterio.open(output_path, "w", **output_meta) as dst: + dst.write(output_array, 1) + dst.set_band_description(1, "ch_class") + + return str(output_path) + + +@app.task(bind=True) +def tree_health_ch_raster_local( + self, + state=None, + district=None, + block=None, + roi=None, + asset_suffix=None, + start_year=None, + end_year=None, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + ch_dir=LOCAL_CH_BASE_DIR, + lulc_dir=LULC_BASE_DIR, + push_to_geoserver=True, + sync_layer_metadata=True, +): + state = str(state).strip().lower() if state else None + district = str(district).strip().lower() if district else None + block = str(block).strip().lower() if block else None + start_year = int(start_year) + end_year = int(end_year) + + if end_year < start_year: + raise ValueError("end_year must be greater than or equal to start_year") + + # Admin runs use the precomputed watershed boundary. Custom runs use the ROI path. + if state and district and block: + asset_suffix = ( + f"{_slug(district, 'unknown_district')}_" + f"{_slug(block, 'unknown_block')}" + ) + roi_gdf = load_precomputed_roi( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + else: + if not roi or not asset_suffix: + raise ValueError( + "For non state/district/block runs, both `roi` and " + "`asset_suffix` are required." + ) + asset_suffix = _slug(asset_suffix, "custom") + roi_gdf = read_validated_vector_file( + roi, + f"ROI file has no valid geometries: {roi}", + ) + + lulc_paths = resolve_lulc_raster_paths( + start_year=start_year, + end_year=end_year, + lulc_dir=lulc_dir, + ) + + layer_at_geoserver = True + + for year, lulc_path in zip(range(start_year, end_year + 1), lulc_paths): + layer_name = f"ch_raster_{asset_suffix}_{year}" + ch_path = _resolve_ch_raster(year=year, ch_dir=ch_dir) + output_path = build_output_raster_path( + layer_name=layer_name, + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + state=state, + district=district, + block=block, + custom_subdir=asset_suffix, + ) + + # Local replacement for: + # ImageCollection(CH).mean().clip(roi).updateMask(lulc.eq(6)) + raster_path = _clip_and_mask_ch( + ch_path=ch_path, + lulc_path=lulc_path, + roi_gdf=roi_gdf, + output_path=output_path, + ) + print(f"Saved local canopy height raster: {raster_path}") + + layer_id = None + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=raster_path, + dataset_name="Canopy Height Raster", + misc={ + "start_year": start_year, + "end_year": end_year, + "is_generated_locally": True, + }, + algorithm="local_ch_clip_tree_mask", + algorithm_version="local-1.0", + ) + + if not push_to_geoserver: + continue + + try: + upload_res, style_res = push_local_raster_to_geoserver( + file_path=raster_path, + layer_name=layer_name, + workspace=GEOSERVER_WORKSPACE, + style_name=GEOSERVER_STYLE, + ) + print(f"GeoServer upload response for {layer_name}: {upload_res}") + print(f"GeoServer style response for {layer_name}: {style_res}") + except Exception as error: + print(f"Failed to sync local CH raster {layer_name}: {error}") + layer_at_geoserver = False + continue + + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + + return layer_at_geoserver if push_to_geoserver else True \ No newline at end of file diff --git a/computing/tree_health/local/canopy_height_vector_local.py b/computing/tree_health/local/canopy_height_vector_local.py new file mode 100644 index 00000000..01e4e7b0 --- /dev/null +++ b/computing/tree_health/local/canopy_height_vector_local.py @@ -0,0 +1,174 @@ +from pathlib import Path + +from computing.config_loader import PROJECT_ROOT +from computing.local_compute_helper import ( + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + build_output_vector_path, + compute_categorical_raster_areas_for_watersheds, + load_precomputed_watersheds, + push_local_vector_to_geoserver, + read_validated_vector_file, + write_vector_output, +) +from computing.utils import save_layer_info_to_db, update_layer_sync_status +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text + +from computing.tree_health.local.canopy_height_local import ( + LOCAL_OUTPUT_BASE_DIR as CH_RASTER_DIR, +) + + +LOCAL_OUTPUT_BASE_DIR = PROJECT_ROOT / "data/tree_health" +GEOSERVER_WORKSPACE = "canopy_height_vector" + +CH_CLASSES = [ + {"value": 0, "label_prefix": "Short_Trees_"}, + {"value": 1, "label_prefix": "Medium_Height_Trees_"}, + {"value": 2, "label_prefix": "Tall_Trees_"}, + {"value": 3, "label_prefix": "Missing_Data_"}, +] + + +def _slug(value, fallback): + if value is None: + return fallback + return valid_gee_text(str(value).strip().lower()) or fallback + + +def _resolve_ch_output_raster(asset_suffix, year, state=None, district=None, block=None): + raster_name = f"ch_raster_{asset_suffix}_{year}.tif" + if state and district and block: + path = ( + Path(CH_RASTER_DIR) + / _slug(state, "unknown_state") + / _slug(district, "unknown_district") + / _slug(block, "unknown_block") + / raster_name + ) + else: + path = Path(CH_RASTER_DIR) / asset_suffix / raster_name + + if path.exists(): + return str(path) + + raise FileNotFoundError( + f"Local canopy height raster not found for vectorisation: {path}. " + "Run canopy_height_local.py first." + ) + +@app.task(bind=True) +def tree_health_ch_vector_local( + self, + state=None, + district=None, + block=None, + roi=None, + asset_suffix=None, + start_year=None, + end_year=None, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + push_to_geoserver=True, + sync_layer_metadata=True, +): + state = str(state).strip().lower() if state else None + district = str(district).strip().lower() if district else None + block = str(block).strip().lower() if block else None + start_year = int(start_year) + end_year = int(end_year) + + if end_year < start_year: + raise ValueError("end_year must be greater than or equal to start_year") + + # Vector outputs are generated over watershed polygons, same as reduceRegions in GEE. + if state and district and block: + asset_suffix = ( + f"{_slug(district, 'unknown_district')}_" + f"{_slug(block, 'unknown_block')}" + ) + result_gdf, _ = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + else: + if not roi or not asset_suffix: + raise ValueError( + "For non state/district/block runs, both `roi` and " + "`asset_suffix` are required." + ) + asset_suffix = _slug(asset_suffix, "custom") + result_gdf = read_validated_vector_file( + roi, + f"ROI file has no valid geometries: {roi}", + ) + + for year in range(start_year, end_year + 1): + raster_path = _resolve_ch_output_raster( + asset_suffix=asset_suffix, + year=year, + state=state, + district=district, + block=block, + ) + class_definitions = [ + {"value": item["value"], "label": f"{item['label_prefix']}{year}"} + for item in CH_CLASSES + ] + print(f"Computing local CH vector columns for {year}: {raster_path}") + year_gdf = compute_categorical_raster_areas_for_watersheds( + watersheds_gdf=result_gdf, + raster_path=raster_path, + class_definitions=class_definitions, + ) + for definition in class_definitions: + result_gdf[definition["label"]] = year_gdf[definition["label"]] + + layer_name = f"ch_vector_{asset_suffix}_{start_year}_{end_year}" + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + custom_subdir=asset_suffix, + ) + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local canopy height vector: {asset_id}") + + if push_to_geoserver: + res = push_local_vector_to_geoserver( + path=asset_id, + layer_name=layer_name, + workspace=GEOSERVER_WORKSPACE, + file_type="gpkg", + ) + print(f"GeoServer response for {layer_name}: {res}") + if not isinstance(res, dict) or res.get("status_code") not in (200, 201): + return False + + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Canopy Height Vector", + misc={ + "start_year": start_year, + "end_year": end_year, + "is_generated_locally": True, + }, + algorithm="local_ch_vector", + algorithm_version="local-1.0", + ) + if layer_id and push_to_geoserver: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + + return True diff --git a/computing/tree_health/local/ccd_local.py b/computing/tree_health/local/ccd_local.py new file mode 100644 index 00000000..2fe4e7cc --- /dev/null +++ b/computing/tree_health/local/ccd_local.py @@ -0,0 +1,273 @@ +from pathlib import Path + +import numpy as np +import rasterio +from rasterio.mask import mask +from rasterio.warp import Resampling, reproject +from shapely.geometry import mapping + +from computing.config_loader import LULC_BASE_DIR, PROJECT_ROOT +from computing.local_compute_helper import ( + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + build_output_raster_path, + get_union_geometry, + load_precomputed_roi, + push_local_raster_to_geoserver, + read_validated_vector_file, + resolve_lulc_raster_paths, + validate_geometry, +) +from computing.utils import save_layer_info_to_db, update_layer_sync_status +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text + +LOCAL_CCD_BASE_DIR = PROJECT_ROOT / "data/base_layers/tree_health/ccd" +LOCAL_OUTPUT_BASE_DIR = PROJECT_ROOT / "data/tree_health" +GEOSERVER_WORKSPACE = "tree_ccd_raster" +GEOSERVER_STYLE = "tree_ccd_style" + +# LULC class 6 is tree cover. CCD values are retained only where this mask is true. +TREE_LULC_CLASS = 6 + + +def _slug(value, fallback): + # Keep layer/file names compatible with the naming style used by GEE tasks. + if value is None: + return fallback + return valid_gee_text(str(value).strip().lower()) or fallback + + +def _resolve_ccd_raster(year, ccd_dir=LOCAL_CCD_BASE_DIR): + # Local CCD files are expected to be pre-downloaded in the base layer folder. + print(ccd_dir) + ccd_dir = Path(ccd_dir) + path = ccd_dir / f"CCD_raster_{year}.tif" + print(path) + if path.exists(): + return str(path) + + raise FileNotFoundError(f"Local CCD raster for {year} not found in {ccd_dir}. ") + + +def _pick_output_nodata(dtype, source_nodata): + # CCD has valid class values 0, 1 and 2, so avoid using those as nodata. + if source_nodata is not None: + source_nodata = float(source_nodata) + if not np.isnan(source_nodata) and source_nodata not in (0.0, 1.0, 2.0): + return source_nodata + + dtype = np.dtype(dtype) + if np.issubdtype(dtype, np.floating): + return -9999.0 + + info = np.iinfo(dtype) + if np.issubdtype(dtype, np.signedinteger): + return info.min + return info.max + + +def _clip_and_mask_ccd(ccd_path, lulc_path, roi_gdf, output_path): + with rasterio.open(ccd_path) as ccd_src: + # Match the ROI CRS to the CCD raster before clipping. + roi_gdf = validate_geometry(roi_gdf) + if roi_gdf.empty: + raise ValueError("No valid ROI geometry available for local CCD clipping.") + if roi_gdf.crs is None: + raise ValueError("ROI CRS is missing; cannot align CCD raster.") + if ccd_src.crs and roi_gdf.crs != ccd_src.crs: + roi_gdf = roi_gdf.to_crs(ccd_src.crs) + + roi_union = get_union_geometry(roi_gdf) + if roi_union is None or roi_union.is_empty: + raise ValueError("ROI union geometry is empty for local CCD clipping.") + + # Prefer the band named "cc" if present; otherwise use the first band. + band_index = 1 + for index, description in enumerate(ccd_src.descriptions, start=1): + if description and description.strip().lower() == "cc": + band_index = index + break + + nodata = _pick_output_nodata( + dtype=ccd_src.dtypes[band_index - 1], + source_nodata=ccd_src.nodata, + ) + + # Clip CCD to the block/ROI boundary and keep the clipped metadata for output. + ccd_array, ccd_transform = mask( + ccd_src, + shapes=[mapping(roi_union)], + crop=True, + filled=True, + nodata=nodata, + indexes=band_index, + ) + if ccd_array.ndim == 3: + ccd_array = ccd_array[0] + + output_meta = ccd_src.meta.copy() + output_meta.update( + { + "driver": "GTiff", + "height": ccd_array.shape[0], + "width": ccd_array.shape[1], + "transform": ccd_transform, + "count": 1, + "dtype": ccd_array.dtype, + "nodata": nodata, + "compress": "lzw", + } + ) + + # Reproject local LULC to the clipped CCD grid so both arrays line up pixel-to-pixel. + lulc_array = np.zeros((output_meta["height"], output_meta["width"]), dtype=np.uint8) + with rasterio.open(lulc_path) as lulc_src: + reproject( + source=rasterio.band(lulc_src, 1), + destination=lulc_array, + src_transform=lulc_src.transform, + src_crs=lulc_src.crs, + src_nodata=lulc_src.nodata, + dst_transform=output_meta["transform"], + dst_crs=output_meta["crs"], + dst_nodata=0, + resampling=Resampling.mode, + ) + + # Keep CCD values only for tree pixels. Everything else becomes nodata. + tree_mask = lulc_array == TREE_LULC_CLASS + + valid_ccd = ccd_array != nodata + output_array = np.where(tree_mask & valid_ccd, ccd_array, nodata).astype( + ccd_array.dtype, + copy=False, + ) + + # Final output is a single-band GeoTIFF, same as the GEE-created CCD raster. + with rasterio.open(output_path, "w", **output_meta) as dst: + dst.write(output_array, 1) + dst.set_band_description(1, "cc") + + return str(output_path) + + +@app.task(bind=True) +def tree_health_ccd_raster_local( + self, + state=None, + district=None, + block=None, + roi=None, + asset_suffix=None, + start_year=None, + end_year=None, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + ccd_dir=LOCAL_CCD_BASE_DIR, + lulc_dir=LULC_BASE_DIR, + push_to_geoserver=True, + sync_layer_metadata=True, +): + state = str(state).strip().lower() if state else None + district = str(district).strip().lower() if district else None + block = str(block).strip().lower() if block else None + start_year = int(start_year) + end_year = int(end_year) + + if end_year < start_year: + raise ValueError("end_year must be greater than or equal to start_year") + + # Admin runs use the precomputed watershed boundary. Custom runs use the ROI path. + if state and district and block: + asset_suffix = ( + f"{_slug(district, 'unknown_district')}_" f"{_slug(block, 'unknown_block')}" + ) + roi_gdf = load_precomputed_roi( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + else: + if not roi or not asset_suffix: + raise ValueError( + "For non state/district/block runs, both `roi` and " + "`asset_suffix` are required." + ) + asset_suffix = _slug(asset_suffix, "custom") + roi_gdf = read_validated_vector_file( + roi, + f"ROI file has no valid geometries: {roi}", + ) + + # LULC rasters provide the yearly tree mask for the same hydro-year range. + lulc_paths = resolve_lulc_raster_paths( + start_year=start_year, + end_year=end_year, + lulc_dir=lulc_dir, + ) + + layer_at_geoserver = False + + for year, lulc_path in zip(range(start_year, end_year + 1), lulc_paths): + # Build one CCD output per year, matching the original GEE layer naming. + layer_name = f"ccd_raster_{asset_suffix}_{year}" + ccd_path = _resolve_ccd_raster(year=year, ccd_dir=ccd_dir) + output_path = build_output_raster_path( + layer_name=layer_name, + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + state=state, + district=district, + block=block, + custom_subdir=asset_suffix, + ) + + # This is the local replacement for: + # ImageCollection(CCD).mean().clip(roi).updateMask(lulc.eq(6)) + raster_path = _clip_and_mask_ccd( + ccd_path=ccd_path, + lulc_path=lulc_path, + roi_gdf=roi_gdf, + output_path=output_path, + ) + print(f"Saved local CCD raster: {raster_path}") + + layer_id = None + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=raster_path, + dataset_name="Ccd Raster", + misc={ + "start_year": start_year, + "end_year": end_year, + "is_generated_locally": True, + }, + algorithm="local_ccd_clip_tree_mask", + algorithm_version="local-1.0", + ) + + if not push_to_geoserver: + continue + + try: + upload_res, style_res = push_local_raster_to_geoserver( + file_path=raster_path, + layer_name=layer_name, + workspace=GEOSERVER_WORKSPACE, + style_name=GEOSERVER_STYLE, + ) + print(f"GeoServer upload response for {layer_name}: {upload_res}") + print(f"GeoServer style response for {layer_name}: {style_res}") + layer_at_geoserver = True + except Exception as error: + print(f"Failed to sync local CCD raster {layer_name}: {error}") + layer_at_geoserver = False + continue + + if layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + + return layer_at_geoserver if push_to_geoserver else True diff --git a/computing/tree_health/local/ccd_vector_local.py b/computing/tree_health/local/ccd_vector_local.py new file mode 100644 index 00000000..a86a7fe8 --- /dev/null +++ b/computing/tree_health/local/ccd_vector_local.py @@ -0,0 +1,171 @@ +from pathlib import Path + +from computing.config_loader import PROJECT_ROOT +from computing.local_compute_helper import ( + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + build_output_vector_path, + compute_categorical_raster_areas_for_watersheds, + load_precomputed_watersheds, + push_local_vector_to_geoserver, + read_validated_vector_file, + write_vector_output, +) +from computing.utils import save_layer_info_to_db, update_layer_sync_status +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text + +from computing.tree_health.local.ccd_local import LOCAL_OUTPUT_BASE_DIR as CCD_RASTER_DIR + + +LOCAL_OUTPUT_BASE_DIR = PROJECT_ROOT / "data/tree_health" +GEOSERVER_WORKSPACE = "tree_ccd_vector" + +CCD_CLASSES = [ + {"value": 0, "label_prefix": "Low_Density_"}, + {"value": 1, "label_prefix": "High_Density_"}, + {"value": 2, "label_prefix": "Missing_Data_"}, +] + + +def _slug(value, fallback): + if value is None: + return fallback + return valid_gee_text(str(value).strip().lower()) or fallback + + +def _resolve_ccd_output_raster(asset_suffix, year, state=None, district=None, block=None): + raster_name = f"ccd_raster_{asset_suffix}_{year}.tif" + if state and district and block: + path = ( + Path(CCD_RASTER_DIR) + / _slug(state, "unknown_state") + / _slug(district, "unknown_district") + / _slug(block, "unknown_block") + / raster_name + ) + else: + path = Path(CCD_RASTER_DIR) / asset_suffix / raster_name + + if path.exists(): + return str(path) + + raise FileNotFoundError( + f"Local CCD raster not found for vectorisation: {path}. " + "Run ccd_local.py first." + ) + +@app.task(bind=True) +def tree_health_ccd_vector_local( + self, + state=None, + district=None, + block=None, + roi=None, + asset_suffix=None, + start_year=None, + end_year=None, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + push_to_geoserver=True, + sync_layer_metadata=True, +): + state = str(state).strip().lower() if state else None + district = str(district).strip().lower() if district else None + block = str(block).strip().lower() if block else None + start_year = int(start_year) + end_year = int(end_year) + + if end_year < start_year: + raise ValueError("end_year must be greater than or equal to start_year") + + # Vector outputs are generated over watershed polygons, same as reduceRegions in GEE. + if state and district and block: + asset_suffix = ( + f"{_slug(district, 'unknown_district')}_" + f"{_slug(block, 'unknown_block')}" + ) + result_gdf, _ = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + else: + if not roi or not asset_suffix: + raise ValueError( + "For non state/district/block runs, both `roi` and " + "`asset_suffix` are required." + ) + asset_suffix = _slug(asset_suffix, "custom") + result_gdf = read_validated_vector_file( + roi, + f"ROI file has no valid geometries: {roi}", + ) + + for year in range(start_year, end_year + 1): + raster_path = _resolve_ccd_output_raster( + asset_suffix=asset_suffix, + year=year, + state=state, + district=district, + block=block, + ) + class_definitions = [ + {"value": item["value"], "label": f"{item['label_prefix']}{year}"} + for item in CCD_CLASSES + ] + print(f"Computing local CCD vector columns for {year}: {raster_path}") + year_gdf = compute_categorical_raster_areas_for_watersheds( + watersheds_gdf=result_gdf, + raster_path=raster_path, + class_definitions=class_definitions, + ) + for definition in class_definitions: + result_gdf[definition["label"]] = year_gdf[definition["label"]] + + layer_name = f"ccd_vector_{asset_suffix}_{start_year}_{end_year}" + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + custom_subdir=asset_suffix, + ) + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local CCD vector: {asset_id}") + + if push_to_geoserver: + res = push_local_vector_to_geoserver( + path=asset_id, + layer_name=layer_name, + workspace=GEOSERVER_WORKSPACE, + file_type="gpkg", + ) + print(f"GeoServer response for {layer_name}: {res}") + if not isinstance(res, dict) or res.get("status_code") not in (200, 201): + return False + + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Ccd Vector", + misc={ + "start_year": start_year, + "end_year": end_year, + "is_generated_locally": True, + }, + algorithm="local_ccd_vector", + algorithm_version="local-1.0", + ) + if layer_id and push_to_geoserver: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + + return True diff --git a/computing/tree_health/local/overall_change_local.py b/computing/tree_health/local/overall_change_local.py new file mode 100644 index 00000000..7924d466 --- /dev/null +++ b/computing/tree_health/local/overall_change_local.py @@ -0,0 +1,315 @@ +from pathlib import Path + +import numpy as np +import rasterio +from rasterio.mask import mask +from rasterio.warp import Resampling, reproject +from shapely.geometry import mapping + +from computing.config_loader import CHANGE_DETECTION_RASTER_OUTPUT_DIR, PROJECT_ROOT +from computing.local_compute_helper import ( + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + build_output_raster_path, + get_union_geometry, + load_precomputed_roi, + push_local_raster_to_geoserver, + read_validated_vector_file, + validate_geometry, +) +from computing.utils import save_layer_info_to_db, update_layer_sync_status +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text + + +LOCAL_TREE_CHANGE_BASE_DIR = PROJECT_ROOT / "data/base_layers/tree_health" +LOCAL_OUTPUT_BASE_DIR = PROJECT_ROOT / "data/tree_health" +GEOSERVER_WORKSPACE = "tree_overall_raster" +GEOSERVER_STYLE = "tree_overall_style" +BACKGROUND = -9999 + + +def _slug(value, fallback): + if value is None: + return fallback + return valid_gee_text(str(value).strip().lower()) or fallback + + +def _resolve_tree_change_raster( + start_year, + end_year, + tree_change_dir=LOCAL_TREE_CHANGE_BASE_DIR, +): + # Local equivalent of TREE_OVERALL_CHANGE image collection. + tree_change_dir = Path(tree_change_dir) + possible_names = [ + f"overall_change_{start_year}_{end_year}.tif", + f"overall_change_{start_year}_{int(end_year) + 1}.tif", + "overall_change.tif", + "TREE_OVERALL_CHANGE.tif", + ] + + for name in possible_names: + path = tree_change_dir / name + if path.exists(): + return str(path) + + matches = sorted(tree_change_dir.glob("*.tif")) + if matches: + return str(matches[0]) + + raise FileNotFoundError(f"Local overall change raster not found in {tree_change_dir}.") + + +def _resolve_change_detection_raster( + state, + district, + block, + asset_suffix, + param_name, + start_year, + end_year, + change_dir=CHANGE_DETECTION_RASTER_OUTPUT_DIR, +): + # Original GEE code reads change outputs ending at end_year + 1. + candidate_end_years = [int(end_year) + 1, int(end_year)] + + for candidate_end_year in candidate_end_years: + raster_name = f"change_{asset_suffix}_{param_name}_{start_year}_{candidate_end_year}.tif" + path = ( + Path(change_dir) + / _slug(state, "unknown_state") + / _slug(district, "unknown_district") + / _slug(block, "unknown_block") + / raster_name + ) + if path.exists(): + return str(path) + + raise FileNotFoundError( + f"Local {param_name} change raster not found for " + f"{asset_suffix}, start_year={start_year}, end_year={end_year}." + ) + + +def _clip_tree_change(tree_change_path, roi_gdf): + with rasterio.open(tree_change_path) as src: + roi_gdf = validate_geometry(roi_gdf) + if roi_gdf.empty: + raise ValueError("No valid ROI geometry available for overall change clipping.") + if roi_gdf.crs is None: + raise ValueError("ROI CRS is missing; cannot align overall change raster.") + if src.crs and roi_gdf.crs != src.crs: + roi_gdf = roi_gdf.to_crs(src.crs) + + roi_union = get_union_geometry(roi_gdf) + if roi_union is None or roi_union.is_empty: + raise ValueError("ROI union geometry is empty for overall change clipping.") + + tree_change, transform = mask( + src, + shapes=[mapping(roi_union)], + crop=True, + filled=True, + nodata=BACKGROUND, + indexes=1, + ) + if tree_change.ndim == 3: + tree_change = tree_change[0] + + meta = src.meta.copy() + meta.update( + { + "driver": "GTiff", + "height": tree_change.shape[0], + "width": tree_change.shape[1], + "transform": transform, + "count": 1, + "dtype": "int16", + "nodata": BACKGROUND, + "compress": "lzw", + } + ) + + tree_change = np.where(np.isfinite(tree_change), tree_change, BACKGROUND) + return np.rint(tree_change).astype(np.int16), meta + + +def _reproject_to_match(raster_path, meta): + output = np.full((meta["height"], meta["width"]), BACKGROUND, dtype=np.int16) + with rasterio.open(raster_path) as src: + reproject( + source=rasterio.band(src, 1), + destination=output, + src_transform=src.transform, + src_crs=src.crs, + src_nodata=src.nodata, + dst_transform=meta["transform"], + dst_crs=meta["crs"], + dst_nodata=BACKGROUND, + resampling=Resampling.mode, + ) + return output + + +def _build_overall_change_raster( + tree_change_path, + deforestation_path, + afforestation_path, + roi_gdf, + output_path, +): + tree_change, output_meta = _clip_tree_change( + tree_change_path=tree_change_path, + roi_gdf=roi_gdf, + ) + deforestation = _reproject_to_match(deforestation_path, output_meta) + afforestation = _reproject_to_match(afforestation_path, output_meta) + + # Same class priority as mask_raster() in the GEE implementation. + output = np.full(tree_change.shape, BACKGROUND, dtype=np.int16) + + no_change_mask = afforestation == 1 + output[no_change_mask] = 0 + + deforestation_mask = (deforestation >= 2) & (deforestation <= 5) + output[deforestation_mask] = -2 + + afforestation_mask = (afforestation >= 2) & (afforestation <= 5) + output[afforestation_mask] = 2 + + allowed_inside_no_change = np.isin(tree_change, [-1, 1, 3, 4, 5]) + output[no_change_mask & allowed_inside_no_change] = tree_change[ + no_change_mask & allowed_inside_no_change + ] + + with rasterio.open(output_path, "w", **output_meta) as dst: + dst.write(output, 1) + dst.set_band_description(1, "constant") + + return str(output_path) + +@app.task(bind=True) +def tree_health_overall_change_raster_local( + self, + state=None, + district=None, + block=None, + start_year=None, + end_year=None, + roi=None, + asset_suffix=None, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + tree_change_dir=LOCAL_TREE_CHANGE_BASE_DIR, + change_dir=CHANGE_DETECTION_RASTER_OUTPUT_DIR, + deforestation_path=None, + afforestation_path=None, + push_to_geoserver=True, + sync_layer_metadata=True, +): + state = str(state).strip().lower() if state else None + district = str(district).strip().lower() if district else None + block = str(block).strip().lower() if block else None + start_year = int(start_year) + end_year = int(end_year) + + if state and district and block: + asset_suffix = ( + f"{_slug(district, 'unknown_district')}_" + f"{_slug(block, 'unknown_block')}" + ) + roi_gdf = load_precomputed_roi( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + else: + if not roi or not asset_suffix: + raise ValueError( + "For non state/district/block runs, both `roi` and " + "`asset_suffix` are required." + ) + asset_suffix = _slug(asset_suffix, "custom") + roi_gdf = read_validated_vector_file( + roi, + f"ROI file has no valid geometries: {roi}", + ) + + layer_name = f"overall_change_raster_{asset_suffix}" + tree_change_path = _resolve_tree_change_raster( + start_year=start_year, + end_year=end_year, + tree_change_dir=tree_change_dir, + ) + if state and district and block: + deforestation_path = deforestation_path or _resolve_change_detection_raster( + state=state, + district=district, + block=block, + asset_suffix=asset_suffix, + param_name="Deforestation", + start_year=start_year, + end_year=end_year, + change_dir=change_dir, + ) + afforestation_path = afforestation_path or _resolve_change_detection_raster( + state=state, + district=district, + block=block, + asset_suffix=asset_suffix, + param_name="Afforestation", + start_year=start_year, + end_year=end_year, + change_dir=change_dir, + ) + elif not deforestation_path or not afforestation_path: + raise ValueError( + "For custom overall change runs, `deforestation_path` and " + "`afforestation_path` are required." + ) + output_path = build_output_raster_path( + layer_name=layer_name, + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + state=state, + district=district, + block=block, + custom_subdir=asset_suffix, + ) + + raster_path = _build_overall_change_raster( + tree_change_path=tree_change_path, + deforestation_path=deforestation_path, + afforestation_path=afforestation_path, + roi_gdf=roi_gdf, + output_path=output_path, + ) + print(f"Saved local overall tree change raster: {raster_path}") + + layer_id = None + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=raster_path, + dataset_name="Tree Overall Change Raster", + misc={"is_generated_locally": True}, + algorithm="local_tree_overall_change", + algorithm_version="local-1.0", + ) + + if push_to_geoserver: + upload_res, style_res = push_local_raster_to_geoserver( + file_path=raster_path, + layer_name=layer_name, + workspace=GEOSERVER_WORKSPACE, + style_name=GEOSERVER_STYLE, + ) + print(f"GeoServer upload response for {layer_name}: {upload_res}") + print(f"GeoServer style response for {layer_name}: {style_res}") + + if layer_id and push_to_geoserver: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + + return True diff --git a/computing/tree_health/local/overall_change_vector_local.py b/computing/tree_health/local/overall_change_vector_local.py new file mode 100644 index 00000000..64152164 --- /dev/null +++ b/computing/tree_health/local/overall_change_vector_local.py @@ -0,0 +1,159 @@ +from pathlib import Path + +from computing.config_loader import PROJECT_ROOT +from computing.local_compute_helper import ( + PRECOMPUTED_TEHSIL_WATERSHED_DIR, + build_output_vector_path, + compute_categorical_raster_areas_for_watersheds, + load_precomputed_watersheds, + push_local_vector_to_geoserver, + read_validated_vector_file, + write_vector_output, +) +from computing.utils import save_layer_info_to_db, update_layer_sync_status +from nrm_app.celery import app +from utilities.gee_utils import valid_gee_text + +from computing.tree_health.local.overall_change_local import ( + LOCAL_OUTPUT_BASE_DIR as OVERALL_CHANGE_RASTER_DIR, +) + +LOCAL_OUTPUT_BASE_DIR = PROJECT_ROOT / "data/tree_health" +GEOSERVER_WORKSPACE = "tree_overall_vector" + +OVERALL_CHANGE_CLASSES = [ + {"value": -2, "label": "Deforestation"}, + {"value": -1, "label": "Degradation"}, + {"value": 0, "label": "No_Change"}, + {"value": 1, "label": "Improvement"}, + {"value": 2, "label": "Afforestation"}, + {"values": [3, 4], "label": "Partially_Degraded"}, + {"value": 5, "label": "Missing Data"}, +] + + +def _slug(value, fallback): + if value is None: + return fallback + return valid_gee_text(str(value).strip().lower()) or fallback + + +def _resolve_overall_change_raster(asset_suffix, state=None, district=None, block=None): + raster_name = f"overall_change_raster_{asset_suffix}.tif" + if state and district and block: + path = ( + Path(OVERALL_CHANGE_RASTER_DIR) + / _slug(state, "unknown_state") + / _slug(district, "unknown_district") + / _slug(block, "unknown_block") + / raster_name + ) + else: + path = Path(OVERALL_CHANGE_RASTER_DIR) / asset_suffix / raster_name + + if path.exists(): + return str(path) + + raise FileNotFoundError( + f"Local overall change raster not found for vectorisation: {path}. " + "Run overall_change_local.py first." + ) + + +@app.task(bind=True) +def tree_health_overall_change_vector_local( + self, + state=None, + district=None, + block=None, + roi=None, + asset_suffix=None, + precomputed_roi_dir=PRECOMPUTED_TEHSIL_WATERSHED_DIR, + push_to_geoserver=True, + sync_layer_metadata=True, +): + state = str(state).strip().lower() if state else None + district = str(district).strip().lower() if district else None + block = str(block).strip().lower() if block else None + + # Vector outputs are generated over watershed polygons, same as reduceRegions in GEE. + if state and district and block: + asset_suffix = ( + f"{_slug(district, 'unknown_district')}_" f"{_slug(block, 'unknown_block')}" + ) + result_gdf, _ = load_precomputed_watersheds( + state=state, + district=district, + block=block, + precomputed_roi_dir=precomputed_roi_dir, + ) + else: + if not roi or not asset_suffix: + raise ValueError( + "For non state/district/block runs, both `roi` and " + "`asset_suffix` are required." + ) + asset_suffix = _slug(asset_suffix, "custom") + result_gdf = read_validated_vector_file( + roi, + f"ROI file has no valid geometries: {roi}", + ) + + raster_path = _resolve_overall_change_raster( + asset_suffix=asset_suffix, + state=state, + district=district, + block=block, + ) + print(f"Computing local overall change vector columns: {raster_path}") + result_gdf = compute_categorical_raster_areas_for_watersheds( + watersheds_gdf=result_gdf, + raster_path=raster_path, + class_definitions=OVERALL_CHANGE_CLASSES, + ) + + layer_name = f"overall_change_vector_{asset_suffix}" + output_path = build_output_vector_path( + layer_name=layer_name, + state=state, + district=district, + block=block, + output_base_dir=LOCAL_OUTPUT_BASE_DIR, + custom_subdir=asset_suffix, + ) + asset_id = write_vector_output( + gdf=result_gdf, + output_path=output_path, + layer_name=layer_name, + ) + print(f"Saved local overall tree change vector: {asset_id}") + + if push_to_geoserver: + res = push_local_vector_to_geoserver( + path=asset_id, + layer_name=layer_name, + workspace=GEOSERVER_WORKSPACE, + file_type="gpkg", + ) + print(f"GeoServer response for {layer_name}: {res}") + if not isinstance(res, dict) or res.get("status_code") not in (200, 201): + return False + + layer_at_geoserver = False + if sync_layer_metadata and state and district and block: + layer_id = save_layer_info_to_db( + state=state, + district=district, + block=block, + layer_name=layer_name, + asset_id=asset_id, + dataset_name="Tree Overall Change Vector", + misc={"is_generated_locally": True}, + algorithm="local_tree_overall_change_vector", + algorithm_version="local-1.0", + ) + if layer_id and push_to_geoserver: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + layer_at_geoserver = True + + return layer_at_geoserver diff --git a/computing/tree_health/ltp_stp/generate_ltp_stp_change_local.py b/computing/tree_health/ltp_stp/generate_ltp_stp_change_local.py new file mode 100644 index 00000000..df8bf639 --- /dev/null +++ b/computing/tree_health/ltp_stp/generate_ltp_stp_change_local.py @@ -0,0 +1,119 @@ +import os +import numpy as np +import rasterio +from computing.config_loader import PROJECT_ROOT +from nrm_app.celery import app + +# Local base directory for input and output raster files. +LOCAL_OUTPUT_BASE_DIR = PROJECT_ROOT / "data/base_layers/tree_health/ltp_stp" + +# Mapping of agro-climatic zones (ACZs) to their output acronyms. +ACZS = { + "Eastern Plateau & Hills Region": "EPAHR", + "Southern Plateau and Hills Region": "SPAHR", + "East Coast Plains & Hills Region": "ECPHR", + "Western Plateau and Hills Region": "WPAHR", + "Central Plateau & Hills Region": "CPAHR", + "Lower Gangetic Plain Region": "LGPR", + "Middle Gangetic Plain Region": "MGPR", + "Upper Gangetic Plain Region": "UGPR", + "Trans Gangetic Plain Region": "TGPR", + "Eastern Himalayan Region": "EHR", + "Western Himalayan Region": "WHR", + "West Coast Plains & Ghat Region": "WCPGR", + "Gujarat Plains & Hills Region": "GPHR", + "Western Dry Region": "WDR", +} + + +@app.task(bind=True) +def generate_ltp_stp_change_local(self, year_1: int, year_2: int, scale=25): + """Generate LTP-STP change rasters for the given year pair. + + For each ACZ, this function loads the LTP raster for year_1 and year_2, + computes the change category, and writes a compact uint8 output raster. + """ + + for acz, acronym in ACZS.items(): + print(f"Processing {acz}") + + # Input file paths for both years. + ltp_file_1 = os.path.join( + LOCAL_OUTPUT_BASE_DIR, + f"{scale}", + f"ltp_{year_1}", + acronym, + f"ltp_{year_1}_{acronym}.tif", + ) + ltp_file_2 = os.path.join( + LOCAL_OUTPUT_BASE_DIR, + f"{scale}", + f"ltp_{year_2}", + acronym, + f"ltp_{year_2}_{acronym}.tif", + ) + + if not (os.path.exists(ltp_file_1) and os.path.exists(ltp_file_2)): + print("Missing input(s)") + continue + + # Read first year raster and preserve profile metadata for output. + with rasterio.open(ltp_file_1) as src1: + ltp1 = src1.read(1) + profile = src1.profile.copy() + nodata = src1.nodata if src1.nodata is not None else 255 + + # Read second year raster. + with rasterio.open(ltp_file_2) as src2: + ltp2 = src2.read(1) + + # Standardize nodata values for comparison. + ltp1 = np.where(ltp1 == nodata, -9999, ltp1) + ltp2 = np.where(ltp2 == nodata, -9999, ltp2) + + # Initialize output change raster with nodata default. + change = np.full(ltp1.shape, -9999, dtype=np.int16) + + # Define change categories: + # 0 -> no change, absent in both years + # 1 -> no change, present in both years + # 2 -> new presence in year_2 + # 3 -> loss from year_1 to year_2 + # 4 -> year_1 nodata, year_2 absent + # 5 -> year_1 nodata, year_2 present + # 6 -> year_1 absent, year_2 nodata + # 7 -> year_1 present, year_2 nodata + change[(ltp1 == 0) & (ltp2 == 0)] = 0 + change[(ltp1 == 1) & (ltp2 == 1)] = 1 + change[(ltp1 == 0) & (ltp2 == 1)] = 2 + change[(ltp1 == 1) & (ltp2 == 0)] = 3 + change[(ltp1 == -9999) & (ltp2 == 0)] = 4 + change[(ltp1 == -9999) & (ltp2 == 1)] = 5 + change[(ltp1 == 0) & (ltp2 == -9999)] = 6 + change[(ltp1 == 1) & (ltp2 == -9999)] = 7 + + # Convert back to original nodata and write as uint8. + change = np.where(change == -9999, nodata, change).astype(np.uint8) + profile.update( + driver="GTiff", dtype="uint8", count=1, compress="lzw", nodata=nodata + ) + + outdir = os.path.join(LOCAL_OUTPUT_BASE_DIR, f"ltp_change_{year_1}_{year_2}") + os.makedirs(outdir, exist_ok=True) + outfile = os.path.join(outdir, f"ltp_change_{year_1}_{year_2}_{acronym}.tif") + + with rasterio.open(outfile, "w", **profile) as dst: + dst.write(change, 1) + + print("Saved:", outfile) + + +# To merge the files in the output directory and run the following command in terminal: +# gdal_merge.py \ +# -o ltp_stp_change_2017_2024.tif \ +# -co COMPRESS=LZW \ +# -co TILED=YES \ +# -co BIGTIFF=YES \ +# -n 255 \ +# -a_nodata 255 \ +# *.tif diff --git a/computing/tree_health/ltp_stp/generate_ltp_stp_local.py b/computing/tree_health/ltp_stp/generate_ltp_stp_local.py new file mode 100644 index 00000000..8825f986 --- /dev/null +++ b/computing/tree_health/ltp_stp/generate_ltp_stp_local.py @@ -0,0 +1,505 @@ +import os +import re + +import geopandas as gpd +import numpy as np +import pandas as pd +import rasterio +from rasterio.mask import mask +from rasterio.features import shapes, rasterize +from shapely.geometry import shape +from rasterio.enums import Resampling +from rasterio.warp import reproject +from computing.config_loader import LULC_BASE_DIR, PROJECT_ROOT +from rasterio.merge import merge +from glob import glob +from math import floor +from rasterio.transform import Affine + +from nrm_app.celery import app + +""" +Generate Long-Term Tree Patches (LTP) and Short-Term Tree Patches (STP) rasters. + +This script processes satellite imagery and land use data to identify and classify +tree patches based on size thresholds. It follows this workflow: + Year + -> ACZ (Agroclimatic Zone) + -> District + -> Clip LULC (Land Use Land Cover) data + -> Extract tree mask + -> Polygonize tree patches + -> Compute patch area in hectares + -> Classify as LTP (Large Tree Patches) or STP (Small Tree Patches) + -> Rasterize classified patches + -> Save GeoTIFF output +""" + + +# LULC classification value for tree/forest cover +TREE_CLASS = 6 + +# Minimum area threshold (in hectares) to classify a patch as Large Tree Patch (LTP) +# Patches below this are classified as Short-term Tree Patches (STP) +AREA_THRESHOLD_HA = 1.0 + +# Base output directory for LTP/STP raster files +LOCAL_OUTPUT_BASE_DIR = PROJECT_ROOT / "data/base_layers/tree_health/ltp_stp" + + +# Dictionary mapping Agroclimatic Zones (ACZ) to their acronyms +# Other ACZs are commented out but available for use + +ACZS = { + "Eastern Plateau & Hills Region": "EPAHR", + "Southern Plateau and Hills Region": "SPAHR", + "East Coast Plains & Hills Region": "ECPHR", + "Western Plateau and Hills Region": "WPAHR", + "Central Plateau & Hills Region": "CPAHR", + "Lower Gangetic Plain Region": "LGPR", + "Middle Gangetic Plain Region": "MGPR", + "Eastern Himalayan Region": "EHR", + "Western Himalayan Region": "WHR", + "Upper Gangetic Plain Region": "UGPR", + "Trans Gangetic Plain Region": "TGPR", + "West Coast Plains & Ghat Region": "WCPGR", + "Gujarat Plains & Hills Region": "GPHR", + "Western Dry Region": "WDR", +} + + +@app.task(bind=True) +def generate_ltp_stp_local(self, start_year, end_year, scale=25): + """ + Main function to generate LTP/STP classification rasters. + + Processes LULC data for a range of years across multiple ACZs and districts. + Creates both starting year and ending year datasets using modal LULC values + from a 3-year window (current year and +/- 1 year for robustness). + + Args: + start_year: Beginning year for analysis period + end_year: End year for analysis period + scale: Scaling factor for LTP/STP + """ + # Load district boundary geometries from GeoJSON + district_boundaries = gpd.read_file( + "data/base_layers/india_district_boundaries.geojson" + ) + print( + "Loaded DISTRICTS:", + len(district_boundaries), + "rows from data/base_layers/india_district_boundaries.geojson", + ) + + # Create 3-year windows for modal LULC calculation (current year ± 1 year) + # This provides temporal stability in the LULC classification + start_years = [start_year, start_year + 1, start_year + 2] + end_years = [end_year, end_year - 1, end_year - 2] + + # Process both start and end year datasets + for lulc_years in [start_years, end_years]: + year = lulc_years[0] + + # Iterate through each Agroclimatic Zone + for acz, acronym in ACZS.items(): + + print(f"\nProcessing {acz}") + + # Load district names that belong to this ACZ + district_csv = ( + f"data/base_layers/tree_health/Agroclimatic_regions/{acz}.csv" + ) + district_names = pd.read_csv(district_csv)["Name"].tolist() + + # Create output directory for this ACZ and year + output_dir = os.path.join( + LOCAL_OUTPUT_BASE_DIR, + f"{scale}", + f"ltp_{year}", + acronym, + ) + os.makedirs(output_dir, exist_ok=True) + + # Open LULC source files for the 3-year window + lulc_sources = [] + + for y in lulc_years: + lulc_file = f"data/base_layers/lulc/lulc_v3_{y}_{y+1}.tif" + lulc_sources.append(rasterio.open(lulc_file)) + + # Generate LTP/STP rasters for each district in the ACZ + generate_district_tiff( + acronym, + district_boundaries, + district_names, + lulc_sources, + output_dir, + year, + scale, + ) + + # Merge individual district rasters into single ACZ mosaic + merge_district_tiffs(acz, output_dir, year, acronym) + + # Close all LULC source files + for src in lulc_sources: + src.close() + + +def get_lulc_mode(lulc_sources, district, scale): + """ + Extract and process LULC data for a district. + + Computes the modal (most common) LULC class from 3-year LULC stack, + resamples to 25m resolution, and extracts tree class pixels. + + Args: + lulc_sources: List of opened rasterio LULC source files (3 files) + district: GeoDataFrame containing the district geometry + scale: Scaling factor for LTP/STP + + Returns: + tree: Binary array (1 = tree, 0 = non-tree) + transform: Affine transform of the output raster + profile: Rasterio profile of the source data + """ + lulc_arrays = [] + + profile = None + transform = None + nodata = None + crs = None + source_transform = None + + # Clip each LULC source to district boundaries and collect arrays + for src in lulc_sources: + roi = district + # Reproject district to match source CRS if needed + if roi.crs != src.crs: + roi = roi.to_crs(src.crs) + + # Clip LULC raster to district boundaries + clipped, transform = mask( + src, + roi.geometry, + crop=True, + filled=False, + indexes=1, + ) + + # Extract metadata from first source + if profile is None: + profile = src.profile.copy() + nodata = src.nodata if src.nodata is not None else 255 + crs = src.crs + source_transform = src.transform + + # Fill masked pixels with nodata value + arr = clipped.filled(nodata) + lulc_arrays.append(arr) + + # Stack the three LULC arrays + stack = np.stack(lulc_arrays) + + # Compute modal LULC using the three-year stack + # Returns the most frequent class value; uses tie-breaking logic + a = stack[0] + b = stack[1] + c = stack[2] + + # Nested where conditions to find the mode: + # If a==b, use a; else if a==c, use a; else if b==c, use b; else use b as tie-breaker + modal = np.where( + a == b, + a, + np.where( + a == c, + a, + np.where( + b == c, + b, + b, # tie breaker - default to middle year + ), + ), + ).astype(np.uint8) + + if scale == 25: + # Resample modal LULC from 10m to 25m resolution + modal, transform = resample_tiff( + modal, + transform, + source_transform, + crs, + nodata, + source_resolution_m=10, + target_resolution_m=25, + ) + + # Extract only tree class pixels as binary mask + tree = (modal == TREE_CLASS).astype(np.uint8) + return tree, transform, profile + + +def resample_tiff( + clipped, + transform, + source_transform, + src_crs, + nodata, + source_resolution_m, + target_resolution_m, +): + """ + Resample raster using MODE resampling. + + Snaps the output to the original LULC grid to ensure alignment with + other datasets. This maintains consistency across multiple processing runs. + + Args: + clipped: Input raster array + transform: Current Affine transform of clipped data + source_transform: Original Affine transform of LULC source + src_crs: Coordinate reference system + nodata: Nodata value + source_resolution_m: Source resolution in m + target_resolution_m: Target resolution in m + Returns: + dst: Resampled raster array + dst_transform: Affine transform of output raster + """ + # Extract coordinates from current transform + left = transform.c + top = transform.f + + # Calculate boundaries of the clipped area + right = left + clipped.shape[1] * transform.a + bottom = top + clipped.shape[0] * transform.e + + # Extract coordinates from original LULC grid + # (ensures alignment with source resolution) + src_left = source_transform.c + src_top = source_transform.f + + # Calculate resampling scale factor (2.5x for 10m -> 25m) + scale = target_resolution_m / source_resolution_m + + # Calculate target pixel resolution + target_res_x = abs(source_transform.a) * scale + target_res_y = abs(source_transform.e) * scale + + # Snap to the original LULC grid - align new pixels to source grid + new_left = src_left + floor((left - src_left) / target_res_x) * target_res_x + new_top = src_top - floor((src_top - top) / target_res_y) * target_res_y + + # Calculate output raster dimensions + dst_width = int(np.ceil((right - new_left) / target_res_x)) + dst_height = int(np.ceil((new_top - bottom) / target_res_y)) + + # Create output Affine transform + dst_transform = Affine( + target_res_x, + 0, + new_left, + 0, + -target_res_y, + new_top, + ) + + # Initialize output array filled with nodata value + dst = np.full( + (dst_height, dst_width), + nodata, + dtype=clipped.dtype, + ) + + # Reproject and resample using MODE resampling + # MODE resampling preserves the most common value in each output pixel + reproject( + source=clipped, + destination=dst, + src_transform=transform, + src_crs=src_crs, + dst_transform=dst_transform, + dst_crs=src_crs, + src_nodata=nodata, + dst_nodata=nodata, + resampling=Resampling.mode, + ) + return dst, dst_transform + + +def generate_district_tiff( + acronym, district_boundaries, district_names, lulc_sources, output_dir, year, scale +): + """ + Generate LTP/STP classification rasters for each district. + + For each district, extracts tree pixels, polygonizes them, calculates + patch areas, and classifies based on size threshold into LTP (Large) + or STP (Small) tree patches. + + Args: + acronym: ACZ acronym for file naming + district_boundaries: GeoDataFrame of all district boundaries + district_names: List of district names to process + lulc_sources: List of opened LULC rasterio objects + output_dir: Directory to save output GeoTIFFs + year: Year for file naming + scale: Scale factor for patch size + """ + for district_name in district_names: + print(" Processing district:", district_name) + + # Extract the specific district from boundary data + district = district_boundaries[district_boundaries["Name"] == district_name] + + if district.empty: + print(" ERROR: District not found in boundary file.") + continue + + # Get modal LULC tree mask for the district + tree, transform, profile = get_lulc_mode(lulc_sources, district, scale) + + # Polygonize tree patches - convert raster to vector polygons + # shapes() returns (geometry, value) tuples for connected regions + polygons = [] + + for geom, value in shapes( + tree, + mask=tree == 1, # Only polygonize tree pixels + transform=transform, + connectivity=8, # Use 8-connectivity (diagonal neighbors included) + ): + if value == 1: + polygons.append(shape(geom)) + + if len(polygons) == 0: + print("No tree patches found for this district.") + continue + + # Create GeoDataFrame from polygons + gdf = gpd.GeoDataFrame( + geometry=polygons, + crs=profile["crs"], + ) + + # Convert to UTM for accurate area calculation + utm = gdf.estimate_utm_crs() + projected = gdf.to_crs(utm) + + # Calculate patch area in hectares + gdf["area_ha"] = projected.area / 10000.0 + + # Classify patches as Large Tree Patch (1) or Small Tree Patch (0) + # based on area threshold + gdf["large_tree_patch"] = (gdf["area_ha"] >= AREA_THRESHOLD_HA).astype(np.uint8) + + # Convert back to original CRS + gdf = gdf.to_crs(profile["crs"]) + + # Rasterize the LTP/STP classification back to raster format + ltp = rasterize( + ( + (geom, value) + for geom, value in zip( + gdf.geometry, + gdf.large_tree_patch, + ) + ), + out_shape=tree.shape, + transform=transform, + fill=0, + dtype=np.uint8, + ) + + # Create output raster: + # 255 = nodata, 1 = large tree patch, 0 = small tree patch/non-tree + output = np.full(tree.shape, 255, dtype=np.uint8) + output[tree == 1] = ltp[tree == 1] + + # Update raster profile with output specifications + profile.update( + driver="GTiff", + height=output.shape[0], + width=output.shape[1], + transform=transform, + count=1, + dtype="uint8", + nodata=255, + compress="lzw", + ) + + # Generate output filename with district name + outfile = os.path.join( + output_dir, + f"ltp_{year}_{acronym}_{re.sub('[^A-Za-z0-9]', '', district_name)}.tif", + ) + + # Write raster to GeoTIFF file + with rasterio.open(outfile, "w", **profile) as dst: + dst.write(output, 1) + + print("Saved:", outfile) + + +def merge_district_tiffs(acz, output_dir, year, acronym): + """ + Merge individual district LTP/STP rasters into a single ACZ mosaic. + + Combines all district GeoTIFFs for an ACZ and year into a single + seamless raster using the 'first' method (no priority/overlap handling). + + Args: + acz: Full name of Agroclimatic Zone (for logging) + output_dir: Directory containing district rasters + year: Year for file naming + acronym: ACZ acronym for output filename + """ + print(f"\nMerging district rasters for {acz}...") + + # Find all district raster files in the output directory + district_tiffs = sorted(glob(os.path.join(output_dir, "*.tif"))) + + if len(district_tiffs) == 0: + print("No district rasters found.") + return + + # Open all district rasters + src_files = [rasterio.open(fp) for fp in district_tiffs] + + # Merge rasters using the 'first' method + # (uses first valid pixel from overlapping areas) + mosaic, out_transform = merge( + src_files, + method="first", + ) + + # Copy metadata from first source file + out_meta = src_files[0].meta.copy() + + # Update metadata for merged output + out_meta.update( + { + "height": mosaic.shape[1], + "width": mosaic.shape[2], + "transform": out_transform, + "compress": "lzw", + } + ) + + # Generate output filename for ACZ-level mosaic + acz_output = os.path.join( + output_dir, + f"ltp_{year}_{acronym}.tif", + ) + + # Write merged raster to GeoTIFF + with rasterio.open(acz_output, "w", **out_meta) as dst: + dst.write(mosaic) + + # Close all source files + for src in src_files: + src.close() + + print(f"Saved ACZ raster: {acz_output}") diff --git a/computing/tree_in_grassland/tree_in_grassland.py b/computing/tree_in_grassland/tree_in_grassland.py index 89fbec9c..7da68ad4 100644 --- a/computing/tree_in_grassland/tree_in_grassland.py +++ b/computing/tree_in_grassland/tree_in_grassland.py @@ -13,7 +13,8 @@ save_layer_info_to_db, update_layer_sync_status, ) -from utilities.constants import GEE_PATHS +from gee_computing.models import GEEAccount +from utilities.constants import GEE_PATHS, AEZ, MWS_DATASET from utilities.gee_utils import ( ee_initialize, check_task_status, @@ -32,16 +33,44 @@ ) +def tree_in_grassland_for_AEZ(aez_no, start_year=None, end_year=None, gee_account_id=7): + ee_initialize(gee_account_id) + aez = ee.FeatureCollection(AEZ) + mwses = ee.FeatureCollection(MWS_DATASET) + + filter_aez = aez.filter(ee.Filter.eq("ae_regcode", aez_no)) + roi = mwses.filterBounds(filter_aez.geometry()) + + asset_suffix = f"AEZ_{aez_no}" + asset_folder_list = ["tree_in_grassland"] + generate_tree_in_grassland_layer( + roi=roi, + asset_suffix=asset_suffix, + asset_folder_list=asset_folder_list, + start_year=start_year, + end_year=end_year, + gee_account_id=gee_account_id, + app_type="tree_in_grassland", + sync_to_db=False, + sync_to_geoserver=False, + ) + + @app.task(bind=True) def generate_tree_in_grassland_layer( - self, - state, - district, - block, - start_year, - end_year, - gee_account_id=None, - app_type="MWS", + self, + state=None, + district=None, + block=None, + roi=None, + asset_suffix=None, + asset_folder_list=None, + start_year=None, + end_year=None, + gee_account_id=None, + app_type="MWS", + sync_to_db=True, + sync_to_geoserver=True, ): """ Generate tree-in-grassland context metrics as a vector layer. @@ -75,45 +104,42 @@ def generate_tree_in_grassland_layer( app_type: str – application type key in GEE_PATHS (default "MWS"). """ # ------------------------------------------------------------------ - # STEP 1: Initialize GEE and set up paths + # Initialize GEE and set up paths # ------------------------------------------------------------------ ee_initialize(gee_account_id) start_year = int(start_year) end_year = int(end_year) - asset_suffix = ( + if state and district and block: + asset_suffix = ( valid_gee_text(district.lower()) + "_" + valid_gee_text(block.lower()) - ) - asset_folder_list = [state, district, block] - - description = f"tree_in_grassland_{asset_suffix}_{start_year}_{end_year}" - layer_name = f"{asset_suffix}_tree_in_grassland" - - asset_id = ( - get_gee_dir_path( - asset_folder_list, - asset_path=GEE_PATHS[app_type]["GEE_ASSET_PATH"], - ) - + description - ) - print(f"Tree in Grassland pipeline started: {asset_id=}") + ) + asset_folder_list = [state, district, block] - # ------------------------------------------------------------------ - # STEP 2: Set up ROI (MWS boundaries from GEE) - # ------------------------------------------------------------------ - roi_path = ( + roi = ee.FeatureCollection( get_gee_dir_path( asset_folder_list, asset_path=GEE_PATHS[app_type]["GEE_ASSET_PATH"], ) + f"filtered_mws_{valid_gee_text(district.lower())}" + f"_{valid_gee_text(block.lower())}_uid" - ) - mws_fc = ee.FeatureCollection(roi_path) + ) + + description = f"tree_in_grassland_{asset_suffix}_{start_year}_{end_year}" + layer_name = f"{asset_suffix}_tree_in_grassland" + + if app_type in GEE_PATHS: + asset_path = GEE_PATHS[app_type]["GEE_ASSET_PATH"] + else: + gee_obj = GEEAccount.objects.get(pk=gee_account_id) + asset_path = f"projects/{gee_obj.name}/assets/" + + asset_id = get_gee_dir_path(asset_folder_list, asset_path=asset_path) + description + print(f"Tree in Grassland pipeline started: {asset_id=}") # ------------------------------------------------------------------ - # STEP 3: Compute tree-in-grassland metrics + # Compute tree-in-grassland metrics # ------------------------------------------------------------------ if not is_gee_asset_exists(asset_id): @@ -211,10 +237,10 @@ def area(mask): } ) - fc = mws_fc.map(compute_stats) + fc = roi.map(compute_stats) # -------------------------------------------------------------- - # STEP 4: Export to GEE + # Export to GEE # -------------------------------------------------------------- task_id = export_vector_asset_to_gee(fc, description, asset_id) if task_id: @@ -222,7 +248,7 @@ def area(mask): print("Tree in Grassland layer exported to GEE.") # ------------------------------------------------------------------ - # STEP 5: Publish to GeoServer and save metadata to DB + # Publish to GeoServer and save metadata to DB # ------------------------------------------------------------------ layer_at_geoserver = _save_to_db_and_sync_to_geoserver( layer_name=layer_name, @@ -233,6 +259,8 @@ def area(mask): state=state, district=district, block=block, + sync_to_db=sync_to_db, + sync_to_geoserver=sync_to_geoserver, ) return layer_at_geoserver @@ -243,20 +271,22 @@ def area(mask): def _save_to_db_and_sync_to_geoserver( - layer_name=None, - asset_id=None, - start_year=None, - end_year=None, - asset_suffix=None, - state=None, - district=None, - block=None, + layer_name=None, + asset_id=None, + start_year=None, + end_year=None, + asset_suffix=None, + state=None, + district=None, + block=None, + sync_to_db=True, + sync_to_geoserver=True, ): """Publish asset to GeoServer and persist metadata to the database.""" print("Tree in Grassland: save_to_db_and_sync_to_geoserver") layer_id = None - if state and district and block: + if sync_to_db and state and district and block: layer_id = save_layer_info_to_db( state=state, district=district, @@ -271,15 +301,16 @@ def _save_to_db_and_sync_to_geoserver( ) make_asset_public(asset_id) - - fc = ee.FeatureCollection(asset_id) - res = sync_fc_to_geoserver(fc, state, layer_name, "tree_in_grassland") - print(res) - - layer_at_geoserver = False - if res["status_code"] == 201 and layer_id: - update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) - print("Tree in Grassland: sync to geoserver flag updated") - layer_at_geoserver = True - - return layer_at_geoserver + if sync_to_geoserver: + layer_at_geoserver = False + fc = ee.FeatureCollection(asset_id) + res = sync_fc_to_geoserver(fc, asset_suffix, layer_name, "tree_in_grassland") + print(res) + + if res["status_code"] == 201 and layer_id: + update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) + print("Tree in Grassland: sync to geoserver flag updated") + layer_at_geoserver = True + + return layer_at_geoserver + return False diff --git a/computing/tree_in_grassland/tree_in_grassland_utils.py b/computing/tree_in_grassland/tree_in_grassland_utils.py index 2c9ef22d..8b07999e 100644 --- a/computing/tree_in_grassland/tree_in_grassland_utils.py +++ b/computing/tree_in_grassland/tree_in_grassland_utils.py @@ -7,6 +7,8 @@ import ee +from utilities.constants import PAN_INDIA_LULC_V3_DATASET + # ---------------------------------------- # PARAMETERS / CONSTANTS # ---------------------------------------- @@ -37,8 +39,6 @@ NEIGHBOR_CLASSES = [k for k in LULC_CLASSES.keys() if k != TREE_CLASS] THRESHOLD = 0.5 # strictly > 50% -PAN_INDIA_LULC_PATH = "projects/corestack-datasets/assets/datasets/LULC_v3_river_basin" - def load_pan_india_lulc(year): """ @@ -54,7 +54,7 @@ def load_pan_india_lulc(year): ee.Image with the 'predicted_label' band, unmasked and cast to Int. """ return ( - ee.Image(f"{PAN_INDIA_LULC_PATH}/pan_india_lulc_v3_{year}_{year + 1}") + ee.Image(f"{PAN_INDIA_LULC_V3_DATASET}{year}_{year + 1}") .select("predicted_label") .unmask(0) .toInt() diff --git a/computing/urls.py b/computing/urls.py index 2248579c..b5bdfe23 100644 --- a/computing/urls.py +++ b/computing/urls.py @@ -40,6 +40,11 @@ api.generate_terrain_descriptor, name="generate_terrain_descriptor", ), + path( + "generate_terrain_compute_all/", + api.generate_terrain_compute_all, + name="generate_terrain_compute_all", + ), path( "generate_terrain_raster/", api.generate_terrain_raster, @@ -247,6 +252,77 @@ path( "refresh_cache//", api.refresh_layer_cache, name="refresh_cache" ), + path("spei/", api.generate_spei, name="spei"), + path( + "drought_resilience_resistance/", + api.drought_resilience_resistance, + name="drought_resilience_resistance", + ), + path( + "rainfall_resilience_resistance/", + api.rainfall_resilience_resistance, + name="rainfall_resilience_resistance", + ), + path( + "forest_fire_resilience_resistance/", + api.forest_fire_resilience_resistance, + name="forest_fire_resilience_resistance", + ), + path( + "high_wind_resilience_resistance/", + api.high_wind_resilience_resistance, + name="high_wind_resilience_resistance", + ), + path( + "generate_dem_raster_vector/", + api.generate_fabdem_raster_vector, + name="generate-dem-raster-vector", + ), + path( + "generate_canal_vector/", + api.generate_canal_vector, + name="generate-canal-vector", + ), + path( + "generate_river_data/", + api.generate_river_data, + name="generate-river-data", + ), + path( + "generate_density_vector/", + api.generate_drainage_density_data, + name="generate-drainage-density-vector", + ), + path( + "generate_antyodaya/", + api.generate_antyodaya, + name="generate_antyodaya", + ), + path( + "generate_livestocks/", + api.generate_livestocks, + name="generate_livestocks", + ), + path( + "generate_soil_health/", + api.generate_soil_health, + name="generate_soil_health", + ), + path( + "generate_soil_type/", + api.generate_soil_type, + name="generate_soil_type", + ), + path( + "generate_ltp_stp/", + api.generate_ltp_stp, + name="generate_ltp_stp", + ), + path( + "generate_ltp_stp_change/", + api.generate_ltp_stp_change, + name="generate_ltp_stp_change", + ), path("missing_excel/", api.missing_excel, name="missing_excel"), path( "generate_tree_in_grassland/", diff --git a/computing/utils.py b/computing/utils.py index 2faf8919..8ec3f274 100644 --- a/computing/utils.py +++ b/computing/utils.py @@ -1,1198 +1,1243 @@ -import copy -import json -import logging -import os -import shutil -import zipfile -from datetime import datetime, timedelta - -import ee -import fiona -import geopandas as gpd -import requests -from django.conf import settings -from shapely.geometry import shape -from shapely.validation import explain_validity - -from computing.models import Dataset, Layer -from geoadmin.models import ( - DistrictSOI, - State_Disritct_Block_Properties, - StateSOI, - TehsilSOI, -) -from projects.models import Project -from utilities.constants import ( - ADMIN_BOUNDARY_OUTPUT_DIR, - GEE_ASSET_PATH, - GEE_HELPER_PATH, - GEE_PATHS, - SHAPEFILE_DIR, -) -from utilities.gee_utils import ( - check_task_status, - ee_initialize, - get_gee_asset_path, - get_gee_dir_path, - get_geojson_from_gcs, - is_asset_public, - is_gee_asset_exists, - sync_vector_to_gcs, - valid_gee_text, -) -from utilities.geoserver_utils import Geoserver -from django.core.mail import EmailMessage, get_connection -import time - - -logger = logging.getLogger(__name__) - - -def generate_shape_files(path): - gdf = gpd.read_file(path + ".json") - if os.path.exists(path): - # Only replace the target shapefile directory. Removing the parent - # state/workspace directory here corrupts sibling outputs on reruns. - shutil.rmtree(path) - - os.makedirs(os.path.dirname(path), exist_ok=True) - gdf.to_file( - path, - driver="ESRI Shapefile", - ) - return path - - -def convert_to_zip(dir_name, file_type): - if file_type == "gpkg": - with zipfile.ZipFile(dir_name + ".zip", "w", zipfile.ZIP_DEFLATED) as zipf: - zipf.write(dir_name + ".gpkg", arcname=os.path.basename(dir_name + ".gpkg")) - return dir_name + ".zip" - else: - return shutil.make_archive(dir_name, "zip", dir_name + "/") - - -def push_shape_to_geoserver( - path, store_name=None, workspace=None, layer_name=None, file_type="shp" -): - geo = Geoserver() - - print(f"layer_name: {layer_name}") - if layer_name: - try: - print(f"Attempting to delete store: {layer_name}") - geo.delete_vector_store(workspace=workspace, store=layer_name) - print(f"Successfully deleted store: {layer_name}") - except Exception as e: - print(f"Store does not exist or error deleting: {str(e)}") - - zip_path = convert_to_zip(path, file_type) - print(f"Zip path: {zip_path}") - print(f"Store name: {store_name}") - print(f"Workspace: {workspace}") - - response = geo.create_shp_datastore( - path=zip_path, - store_name=store_name, - workspace=workspace, - file_extension=file_type, - ) - print(f"Response: {response}") - return response - - -def kml_to_geojson(state_name, district_name, block_name, kml_path): - fiona.drvsupport.supported_drivers["kml"] = ( - "rw" # enable KML support which is disabled by default - ) - fiona.drvsupport.supported_drivers["KML"] = ( - "rw" # enable KML support which is disabled by default - ) - gdf = gpd.read_file(kml_path) - geometry_types = gdf.geometry.geometry.type.unique() - state_dir = os.path.join(ADMIN_BOUNDARY_OUTPUT_DIR, state_name) - - for gtype in geometry_types: - df = gdf.loc[gdf.geometry.geometry.type == gtype] - path = os.path.join(state_dir, f"{district_name}_{block_name}_{gtype}") - df.to_file(path + ".json", driver="GeoJSON") - generate_shape_files(path) - push_shape_to_geoserver(path, workspace="test_workspace") - - -def convert_kml_to_shapefile(kml_path, output_dir, shapefile_name): - if not os.path.exists(output_dir + "/" + shapefile_name): - os.makedirs(output_dir + "/" + shapefile_name) - - shapefile_path = os.path.join( - output_dir + "/" + shapefile_name, shapefile_name + ".shp" - ) - print("path path", shapefile_path) - cmd = f"ogr2ogr -f 'ESRI Shapefile' {shapefile_path} {kml_path}" # output.shp input.kml - os.system(command=cmd) - - return output_dir + "/" + shapefile_name - - -def kml_to_shp(state_name, district_name, block_name, kml_path): - shapefile_name = f"{district_name}_{block_name}" - shapefile_layer_path = convert_kml_to_shapefile( - kml_path, SHAPEFILE_DIR, shapefile_name - ) - - push_shape_to_geoserver(shapefile_layer_path, workspace="customkml") - - # os.remove(kml_path) - # shutil.rmtree(shapefile_layer_path) - os.remove(shapefile_layer_path + ".zip") - - -def sync_layer_to_geoserver(state_name, fc, layer_name, workspace): - state_dir = os.path.join("data/fc_to_shape", state_name) - if not os.path.exists(state_dir): - os.mkdir(state_dir) - path = os.path.join(state_dir, f"{layer_name}") - # Write the feature collection into json file - with open(path + ".json", "w") as f: - try: - f.write(f"{json.dumps(fc)}") - except Exception as e: - print(e) - - path = generate_shape_files(path) - return push_shape_to_geoserver(path, workspace=workspace, layer_name=layer_name) - - -def sync_fc_to_geoserver(fc, shp_folder, layer_name, workspace, style_name=None): - try: - geojson_fc = fc.getInfo() - except Exception as e: - print("Exception in getInfo()", e) - task_id = sync_vector_to_gcs(fc, layer_name, "GeoJSON") - check_task_status([task_id]) - - geojson_fc = get_geojson_from_gcs(layer_name) - geo = Geoserver() - if len(geojson_fc["features"]) > 0: - state_dir = os.path.join("data/fc_to_shape", shp_folder) - if not os.path.exists(state_dir): - os.mkdir(state_dir) - path = os.path.join(state_dir, f"{layer_name}") - - # Convert to GeoDataFrame - gdf = gpd.GeoDataFrame.from_features(geojson_fc["features"]) - - # Set CRS (Earth Engine uses EPSG:4326 by default) - gdf.crs = "EPSG:4326" - - gdf = fix_invalid_geometry_in_gdf(gdf) - - # Save as GeoPackage - gdf.to_file(path + ".gpkg", driver="GPKG") - res = push_shape_to_geoserver(path, workspace=workspace, file_type="gpkg") - if style_name: - style_res = geo.publish_style( - layer_name=layer_name, style_name=style_name, workspace=workspace - ) - print("Style response:", style_res) - return res - else: - return "No features in FeatureCollection" - - -def sync_project_fc_to_geoserver(fc, project_name, layer_name, workspace): - print("inside") - print(layer_name) - try: - geojson_fc = fc.getInfo() - except Exception as e: - print("Exception in getInfo()", e) - task_id = sync_vector_to_gcs(fc, layer_name, "GeoJSON") - check_task_status([task_id]) - - geojson_fc = get_geojson_from_gcs(layer_name) - print(len(geojson_fc["features"])) - if len(geojson_fc["features"]) > 0: - state_dir = os.path.join("data/fc_to_shape", project_name) - if not os.path.exists(state_dir): - os.mkdir(state_dir) - path = os.path.join(state_dir, f"{layer_name}") - - # Convert to GeoDataFrame - gdf = gpd.GeoDataFrame.from_features(geojson_fc["features"]) - - # Set CRS (Earth Engine uses EPSG:4326 by default) - gdf.crs = "EPSG:4326" - - gdf = fix_invalid_geometry_in_gdf(gdf) - - # Save as GeoPackage - gdf.to_file(path + ".gpkg", driver="GPKG") - print("pushed to geoserver") - return push_shape_to_geoserver( - path, workspace=workspace, layer_name=layer_name, file_type="gpkg" - ) - else: - print("no features found") - return - - -def to_camelcase(text): - words = text.split() - camelcase = words[0].lower() - for word in words[1:]: - camelcase += word.capitalize() - return camelcase - - -def create_chunk(aoi, description, chunk_size): - size = aoi.size().getInfo() - parts = size // chunk_size - # task_ids = [] - rois = [] - descs = [] - for part in range(parts + 1): - start = part * chunk_size - end = start + chunk_size - block_name_for_parts = description + "_" + str(start) + "-" + str(end) - roi = ee.FeatureCollection(aoi.toList(aoi.size()).slice(start, end)) - if roi.size().getInfo() > 0: - descs.append(block_name_for_parts) - rois.append(roi) - - return rois, descs - - -def merge_chunks( - aoi, - folder_list, - description, - chunk_size, - chunk_asset_path=GEE_HELPER_PATH, - merge_asset_path=GEE_ASSET_PATH, - merge_asset_id=None, -): - print("Merge Chunk task initiated") - ee_initialize() - size = aoi.size().getInfo() - parts = size // chunk_size - assets = [] - for part in range(parts + 1): - start = part * chunk_size - end = start + chunk_size - block_name_for_parts = description + "_" + str(start) + "-" + str(end) - src_asset_id = ( - get_gee_dir_path(folder_list, chunk_asset_path) + block_name_for_parts - ) - if is_gee_asset_exists(src_asset_id): - assets.append(ee.FeatureCollection(src_asset_id)) - - asset = ee.FeatureCollection(assets).flatten() - - asset_id = merge_asset_id or ( - get_gee_dir_path(folder_list, merge_asset_path) + description - ) - try: - # Export an ee.FeatureCollection as an Earth Engine asset. - task = ee.batch.Export.table.toAsset( - **{ - "collection": asset, - "description": description, - "assetId": asset_id, - } - ) - - task.start() - print("Successfully started the merge chunk", task.status()) - return task.status()["id"] - except Exception as e: - print(f"Error occurred in running merge task: {e}") - return None - - -def fix_invalid_geometry_in_gdf(gdf): - invalid = gdf[~gdf.is_valid] - if not invalid.empty: - print("Invalid geometries found:") - for idx, geom in invalid.geometry.items(): - print(f"Index {idx}: {explain_validity(geom)}") - gdf.loc[idx, "geometry"] = gdf.loc[idx, "geometry"].buffer(0) - - return gdf - - -def get_season_key(date): - """Return season key like 'rabi_2017-2018' based on Indian cropping seasons.""" - month = date.month - year = date.year - next_year = year + 1 - - if month in [1, 2]: - return f"rabi_{year - 1}-{year}" # Jan–Feb → Rabi of previous year - elif month in [11, 12]: - return f"rabi_{year}-{next_year}" # Nov–Dec → Rabi starting this year - elif month in [3, 4, 5, 6]: - return f"zaid_{year}-{next_year}" - elif month in [7, 8, 9, 10]: - return f"kharif_{year}-{next_year}" - else: - return None - - -def get_agri_year_key(season_key): - """Convert a season key to agricultural year key (e.g., rabi_2017-2018 → 2017-2018).""" - season, years = season_key.split("_") - start_year, end_year = map(int, years.split("-")) - - if season in ["kharif", "rabi"]: - return f"{start_year}-{end_year}" - elif season == "zaid": - return f"{start_year - 1}-{start_year}" # Zaid 2018-2019 → Agri year 2017-2018 - else: - return None - - -def calculate_precipitation_season( - geojson_filepath, draught_asset_id, start_year=None, end_year=None -): - if start_year is None or end_year is None: - raise ValueError( - "start_year and end_year are required for calculate_precipitation_season." - ) - start_year = int(start_year) - end_year = int(end_year) - - # Load the GeoJSON file - with open(geojson_filepath, "r") as f: - feature_collection = json.load(f) - - features_ee = [] - - for feature in feature_collection["features"]: - original_props = feature["properties"] - new_props = {} - - # Copy UID - if "uid" in original_props: - new_props["uid"] = original_props["uid"] - - agri_year_totals = {} - - # Parse precipitation date keys - for key, val in original_props.items(): - try: - date = datetime.strptime(key, "%Y-%m-%d") - season_key = get_season_key(date) - if not season_key: - continue - - agri_key = get_agri_year_key(season_key) - if not agri_key: - continue - - agri_start = int(agri_key.split("-")[0]) - if not (start_year <= agri_start <= end_year): - continue - - season = season_key.split("_")[0] # kharif, rabi etc - full_key = f"{season}_{agri_key}" - - agri_year_totals[full_key] = agri_year_totals.get(full_key, 0) + float( - val - ) - - except Exception: - continue - - # Add all seasonal totals to new_props - for agri_key, total in agri_year_totals.items(): - new_props[f"precipitation_{agri_key}"] = total - - # Create EE Feature - geom_ee = ee.Geometry(feature["geometry"]) - feature_ee = ee.Feature(geom_ee, new_props) - features_ee.append(feature_ee) - - # Left side FC - mws_fc = ee.FeatureCollection(features_ee) - - return mws_fc - - -def generate_geojson_with_ci_and_ndvi(zoi_asset, ci_asset, ndvi_asset, proj_id): - # Load project - proj_obj = Project.objects.get(pk=proj_id) - - # Build CI and NDVI asset paths - asset_path_ci = ( - get_gee_dir_path( - [proj_obj.name], asset_path=GEE_PATHS["WATER_REJ"]["GEE_ASSET_PATH"] - ) - + ci_asset - ) - - asset_path_ndvi = ( - get_gee_dir_path( - [proj_obj.name], asset_path=GEE_PATHS["WATER_REJ"]["GEE_ASSET_PATH"] - ) - + ndvi_asset - ) - - # Load FeatureCollections - zoi = ee.FeatureCollection(zoi_asset) - ci = ee.FeatureCollection(asset_path_ci) - ndvi = ee.FeatureCollection(asset_path_ndvi) - - # ------------------------- - # STEP 1: Join ZOI with Cropping Intensity - # ------------------------- - join = ee.Join.inner() - filter = ee.Filter.intersects(leftField=".geo", rightField=".geo") - zoi_ci_joined = join.apply(zoi, ci, filter) - - def merge_zoi_ci(pair): - zoi_feat = ee.Feature(pair.get("primary")) - ci_feat = ee.Feature(pair.get("secondary")) - merged_props = zoi_feat.toDictionary().combine(ci_feat.toDictionary(), True) - return ee.Feature(zoi_feat.geometry(), merged_props) - - zoi_with_ci = ee.FeatureCollection(zoi_ci_joined.map(merge_zoi_ci)) - - # ------------------------- - # STEP 2: Join ZOI+CI with NDVI - # ------------------------- - zoi_ndvi_joined = join.apply(zoi_with_ci, ndvi, filter) - - def merge_zoi_ci_ndvi(pair): - ci_feat = ee.Feature(pair.get("primary")) - ndvi_feat = ee.Feature(pair.get("secondary")) - merged_props = ci_feat.toDictionary().combine(ndvi_feat.toDictionary(), True) - return ee.Feature(ci_feat.geometry(), merged_props) - - final_merged = ee.FeatureCollection(zoi_ndvi_joined.map(merge_zoi_ci_ndvi)) - - # ------------------------- - # STEP 3: Export or Push to GeoServer - # ------------------------- - layer_name = f"WaterRejapp_zoi_{proj_obj.name}_{proj_obj.id}" - sync_project_fc_to_geoserver(final_merged, proj_obj.name, layer_name, "waterrej") - - -def get_directory_size(path): - total_size = 0 - for dirpath, dirnames, filenames in os.walk(path): - for filename in filenames: - file_path = os.path.join(dirpath, filename) - if os.path.isfile(file_path): - total_size += os.path.getsize(file_path) - return total_size - - -def generate_geojson_with_ci_ndvi_ndmi( - zoi_asset, ci_asset, ndvi_asset, ndmi_asset, proj_id -): - # Load project - proj_obj = Project.objects.get(pk=proj_id) - - zoi = ee.FeatureCollection(zoi_asset) - print("Number of features zoi:", zoi.size().getInfo()) - - ci = ee.FeatureCollection(ci_asset) - print("Number of features zoi:", ci.size().getInfo()) - ndvi = ee.FeatureCollection(ndmi_asset) - print("Number of features zoi:", ndvi.size().getInfo()) - ndmi = ee.FeatureCollection(ndmi_asset) - print("Number of features zoi:", ndmi.size().getInfo()) - - # ------------------------- - # STEP 1: Join ZOI with CI - # ------------------------- - join = ee.Join.inner() - filter = ee.Filter.intersects(leftField=".geo", rightField=".geo") - zoi_ci_joined = join.apply(zoi, ci, filter) - - def merge_zoi_ci(pair): - zoi_feat = ee.Feature(pair.get("primary")) - ci_feat = ee.Feature(pair.get("secondary")) - merged_props = zoi_feat.toDictionary().combine(ci_feat.toDictionary(), True) - return ee.Feature(zoi_feat.geometry(), merged_props) # ✅ keep ZOI geom - - zoi_with_ci = ee.FeatureCollection(zoi_ci_joined.map(merge_zoi_ci)) - - # ------------------------- - # STEP 2: Join with NDVI - # ------------------------- - zoi_ndvi_joined = join.apply(zoi_with_ci, ndvi, filter) - - def merge_zoi_ci_ndvi(pair): - prev_feat = ee.Feature(pair.get("primary")) - ndvi_feat = ee.Feature(pair.get("secondary")) - merged_props = prev_feat.toDictionary().combine(ndvi_feat.toDictionary(), True) - return ee.Feature(prev_feat.geometry(), merged_props) # ✅ still ZOI geom - - zoi_ci_ndvi = ee.FeatureCollection(zoi_ndvi_joined.map(merge_zoi_ci_ndvi)) - - # ------------------------- - # STEP 3: Join with NDMI - # ------------------------- - zoi_ndmi_joined = join.apply(zoi_ci_ndvi, ndmi, filter) - - def merge_zoi_ci_ndvi_ndmi(pair): - prev_feat = ee.Feature(pair.get("primary")) - ndmi_feat = ee.Feature(pair.get("secondary")) - merged_props = prev_feat.toDictionary().combine(ndmi_feat.toDictionary(), True) - return ee.Feature(prev_feat.geometry(), merged_props) # ✅ keep ZOI geom - - final_merged = ee.FeatureCollection(zoi_ndmi_joined.map(merge_zoi_ci_ndvi_ndmi)) - - # ------------------------- - # STEP 4: Export or Push to GeoServer - # ------------------------- - layer_name = f"WaterRejapp_zoi_{proj_obj.name}_{proj_obj.id}" - print(layer_name) - sync_project_fc_to_geoserver(final_merged, proj_obj.name, layer_name, "waterrej") - - -def generate_geojson_with_ci_ndvi(zoi_asset, ci_asset, ndvi_asset, proj_id): - # Load project - proj_obj = Project.objects.get(pk=proj_id) - - # Initialize Earth Engine - ee_initialize(4) - - # Load FeatureCollections - zoi = ee.FeatureCollection(zoi_asset) - ci = ee.FeatureCollection(ci_asset) - ndvi = ee.FeatureCollection(ndvi_asset) - - print("ZOI:", zoi.size().getInfo()) - print("CI:", ci.size().getInfo()) - print("NDVI:", ndvi.size().getInfo()) - - # Common join logic on UID - join = ee.Join.inner() - uid_filter = ee.Filter.equals(leftField="UID", rightField="UID") - - # --- Join ZOI + CI --- - zoi_ci_joined = join.apply(zoi, ci, uid_filter) - - def merge_zoi_ci(pair): - zoi_feat = ee.Feature(pair.get("primary")) - ci_feat = ee.Feature(pair.get("secondary")) - merged_props = zoi_feat.toDictionary().combine(ci_feat.toDictionary(), True) - # Keep ZOI geometry only - return ee.Feature(zoi_feat.geometry(), merged_props) - - zoi_with_ci = ee.FeatureCollection(zoi_ci_joined.map(merge_zoi_ci)) - - # --- Join with NDVI --- - zoi_ndvi_joined = join.apply(zoi_with_ci, ndvi, uid_filter) - - def merge_with_ndvi(pair): - base_feat = ee.Feature(pair.get("primary")) - ndvi_feat = ee.Feature(pair.get("secondary")) - merged_props = base_feat.toDictionary().combine(ndvi_feat.toDictionary(), True) - # Always retain ZOI geometry - return ee.Feature(base_feat.geometry(), merged_props) - - merged_final = ee.FeatureCollection(zoi_ndvi_joined.map(merge_with_ndvi)) - - # --- Ensure ZOI geometry retained in all features --- - merged_final = merged_final.map( - lambda f: ee.Feature( - f.setGeometry( - ee.Feature( - zoi.filter(ee.Filter.eq("UID", f.get("UID"))).first() - ).geometry() - ) - ) - ) - - layer_name = f"WaterRejapp_zoi_{proj_obj.name}_{proj_obj.id}" - print(layer_name) - - sync_project_fc_to_geoserver(merged_final, proj_obj.name, layer_name, "waterrej") - - -def save_layer_info_to_db( - state, - district, - block, - layer_name, - asset_id, - dataset_name, - sync_to_geoserver=False, - layer_version="1.0", - algorithm=None, - algorithm_version="1.0", - misc=None, - is_override=False, - is_gee_asset=True, -): - print("inside the save_layer_info_to_db function") - - dataset = Dataset.objects.get(name=dataset_name) - - try: - state_obj = StateSOI.objects.get(state_name__iexact=state) - district_obj = DistrictSOI.objects.get( - district_name__iexact=district, state=state_obj - ) - block_obj = TehsilSOI.objects.get( - tehsil_name__iexact=block, district=district_obj - ) - except Exception as e: - print("Error fetching in state district block:", e) - return - - is_public = is_asset_public(asset_id) if is_gee_asset else False - - # Check if there’s an existing layer - existing_layer = ( - Layer.objects.filter( - dataset=dataset, - layer_name=layer_name, - state=state_obj, - district=district_obj, - block=block_obj, - ) - .order_by("-layer_version") - .first() - ) - - if existing_layer: - if existing_layer.algorithm_version != algorithm_version: - # Algorithm version changed --> create new record with incremented layer_version - new_layer_version = str(float(existing_layer.layer_version) + 1) - print( - f"Algorithm version changed. Creating new layer version: {new_layer_version}" - ) - layer_obj = Layer.objects.create( - dataset=dataset, - layer_name=layer_name, - state=state_obj, - district=district_obj, - block=block_obj, - layer_version=new_layer_version, - algorithm=algorithm, - algorithm_version=algorithm_version, - is_sync_to_geoserver=sync_to_geoserver, - is_public_gee_asset=is_public, - is_override=is_override, - misc=misc, - gee_asset_path=asset_id, - ) - else: - # Algorithm version is same --> update existing layer - print("Algorithm version same. Updating existing layer.") - for field, value in { - "algorithm": algorithm, - "algorithm_version": algorithm_version, - "is_sync_to_geoserver": sync_to_geoserver, - "is_public_gee_asset": is_public, - "is_override": is_override, - "misc": misc, - "gee_asset_path": asset_id, - }.items(): - setattr(existing_layer, field, value) - existing_layer.save() - layer_obj = existing_layer - else: - # No existing record --> create a new one - print("No existing layer found. Creating new one.") - layer_obj = Layer.objects.create( - dataset=dataset, - layer_name=layer_name, - state=state_obj, - district=district_obj, - block=block_obj, - layer_version=layer_version, - algorithm=algorithm, - algorithm_version=algorithm_version, - is_sync_to_geoserver=sync_to_geoserver, - is_public_gee_asset=is_public, - is_override=is_override, - misc=misc, - gee_asset_path=asset_id, - ) - - print(f"Saved layer info (id={layer_obj.id}, version={layer_obj.layer_version})") - return layer_obj.id - - -def get_existing_end_year(dataset_name, layer_name): - """fetch objects from db on the basis of dataset name and layer_name""" - dataset = Dataset.objects.get(name=dataset_name) - layer_obj = Layer.objects.get(dataset=dataset, layer_name=layer_name) - existing_end_date = layer_obj.misc["end_year"] - print("existing_end_date", existing_end_date) - return existing_end_date - - -def get_layer_object(state, district, block, layer_name, dataset_name): - state_obj = StateSOI.objects.get(state_name__iexact=state) - district_obj = DistrictSOI.objects.get( - district_name__iexact=district, state=state_obj - ) - block_obj = TehsilSOI.objects.get(tehsil_name__iexact=block, district=district_obj) - layer_obj = ( - Layer.objects.filter( - state=state_obj, - district=district_obj, - block=block_obj, - layer_name=layer_name, - dataset__name=dataset_name, - ) - .order_by("-layer_version") - .first() - ) - return layer_obj - - -def update_dashboard_geojson( - state=None, - district=None, - block=None, - layer_name=None, - workspace_name=None, - proj_id=None, -): - if state and block and block: - print(f"🔄 Updating GeoJSON for {state}, {district}, {block}") - - # Get related objects - state_obj = StateSOI.objects.get(state_name=state) - district_obj = DistrictSOI.objects.get(district_name=district) - tehsil_obj = TehsilSOI.objects.get(tehsil_name=block) # fixed typo - - # Get or create main record - obj, created = State_Disritct_Block_Properties.objects.get_or_create( - state=state_obj, district=district_obj, tehsil=tehsil_obj - ) - else: - obj = Project.objects.get(pk=proj_id) - - # Map suffix to json_key - suffix_to_key = { - "wb": "wb_geojson", - "zoi": "zoi_geojson", - "mws": "mws_geojson", - } - - # Detect which key this layer corresponds to - json_key = None - for suffix, key in suffix_to_key.items(): - if layer_name == f"{state}_{district}_{block}_{suffix}": - json_key = key - break - - if not json_key: - print(f"⚠️ Layer name {layer_name} did not match any known type.") - return - - # Construct GeoServer URL - waterrej_url = ( - f"https://geoserver.core-stack.org:8443/geoserver/waterrej/ows?" - f"service=WFS&version=1.0.0&request=GetFeature&typeName={workspace_name}:{layer_name}" - f"&outputFormat=application%2Fjson" - ) - - # Load existing dashboard_geojson or create new - if proj_id: - misc = obj.dashboard_geojson or {} - else: - misc = obj.geojson_path or {} - - # Ensure waterrej section exists - if "waterrej" not in misc: - misc["waterrej"] = {} - - # Update or add this specific json_key - misc["waterrej"][json_key] = waterrej_url - - # Save the updated JSON field - obj.dashboard_geojson = misc - obj.save() - - print(f"✅ Added/Updated {json_key} for {state}, {district}, {block}") - - -def clean_geometry(geom): - """ - Clean geometry: - - Dissolve multipolygon → single polygon - - Remove holes automatically - - Fix invalid topology - - Buffer tiny polygons - """ - - # 1. Dissolve multi-polygons and remove holes - geom = geom.dissolve(maxError=1) - - # 2. Fix invalid rings by simplifying slightly (NEVER buffer(0)) - geom = geom.simplify(1) - - # 3. Buffer polygons smaller than 1 pixel (< 900 m²) - area = geom.area() - geom = ee.Algorithms.If( - area.lt(900), - geom.buffer(15), - geom, # ensure raster pixel center is captured - ) - - return ee.Geometry(geom) - - -def safe_reduce_max(image, geom, scale=30): - geom = clean_geometry(geom) - - val = ( - image.unmask(0) - .reduceRegion( - reducer=ee.Reducer.max(), - geometry=geom, - scale=scale, - maxPixels=1e13, - tileScale=4, - bestEffort=True, - ) - .get("b1") - ) - - return ee.Number(ee.Algorithms.If(val, val, 0)) - - -# ------------------------------------------------------ -# SAFE REDUCE MAX FUNCTION -# ------------------------------------------------------ -def safe_reduce_max(image, geom, scale=30): - geom = clean_geometry(geom) - - result = ( - image.unmask(0) - .reduceRegion( - reducer=ee.Reducer.max(), - geometry=geom, - scale=scale, - maxPixels=1e13, - tileScale=4, - bestEffort=True, - ) - .get("b1") - ) - - # Convert null → 0 - return ee.Number(ee.Algorithms.If(result, result, 0)) - - -# ------------------------------------------------------ -# MAIN FUNCTION TO PROCESS SWB LAYER -# ------------------------------------------------------ -def generate_swb_layer_with_max_so_catchment( - roi=None, - app_type="MWS", - asset_suffix=None, - asset_folder=None, - gee_account_id=None, -): - ee_initialize(gee_account_id) - - # Build asset paths - base_path = get_gee_dir_path( - asset_folder, asset_path=GEE_PATHS[app_type]["GEE_ASSET_PATH"] - ) - - so_asset = f"{base_path}stream_order_{asset_suffix}_raster" - ca_asset = f"{base_path}catchment_area_{asset_suffix}_raster" - - # Load rasters - stream_order_band = ee.Image(so_asset).select("b1") - catchment_band = ee.Image(ca_asset).select("b1") - - # Processing per waterbody - def compute_for_feature(feature): - geom = feature.geometry() - - max_so = safe_reduce_max(stream_order_band, geom, scale=30) - max_ca = safe_reduce_max(catchment_band, geom, scale=30) - - return feature.set( - { - "max_stream_order": max_so, - "max_catchment_area": max_ca, - } - ) - - # Map over the feature collection - return roi.map(compute_for_feature) - - -def _get_prod_backend_url(): - return getattr(settings, "PROD_BACKEND_URL", "").rstrip("/") - - -def _get_prod_api_key(): - return getattr(settings, "PROD_BACKEND_API_KEY", "") - - -def _sync_layer_to_prod_db(payload: dict): - prod_url = _get_prod_backend_url() - if not prod_url: - return None - - endpoint = prod_url + "/api/v1/sync_layer_remote/" - try: - response = requests.post( - endpoint, - json=payload, - headers={"X-Api-Key": _get_prod_api_key()}, - timeout=30, - ) - if response.status_code not in (200, 201): - logger.warning( - "Prod DB sync returned %s for layer %s: %s", - response.status_code, - payload.get("layer_name"), - response.text, - ) - return None - layer_id = response.json().get("layer_id") - logger.info( - "Layer %s synced to prod DB (id=%s).", payload.get("layer_name"), layer_id - ) - return layer_id - except requests.RequestException as e: - logger.error( - "Failed to sync layer %s to prod DB: %s", payload.get("layer_name"), e - ) - return None - - -def _update_layer_sync_remote( - layer_id, sync_to_geoserver=None, is_stac_specs_generated=None -): - prod_url = _get_prod_backend_url() - if not prod_url or layer_id is None: - return - - endpoint = prod_url + "/api/v1/update_layer_sync_remote/" - payload = { - "layer_id": layer_id, - "sync_to_geoserver": sync_to_geoserver, - "is_stac_specs_generated": is_stac_specs_generated, - } - try: - response = requests.post( - endpoint, - json=payload, - headers={"X-Api-Key": _get_prod_api_key()}, - timeout=30, - ) - if response.status_code not in (200, 201): - logger.warning( - "Prod layer sync status update returned %s for layer %s: %s", - response.status_code, - layer_id, - response.text, - ) - else: - logger.info("Layer sync status updated on prod DB for id=%s.", layer_id) - except requests.RequestException as e: - logger.error( - "Failed to update layer sync status on prod DB for id=%s: %s", layer_id, e - ) - - -def update_layer_sync_status( - layer_id, sync_to_geoserver=None, is_stac_specs_generated=None -): - if _get_prod_backend_url(): - _update_layer_sync_remote( - layer_id, - sync_to_geoserver=sync_to_geoserver, - is_stac_specs_generated=is_stac_specs_generated, - ) - return layer_id - - try: - layer_obj = Layer.objects.filter(id=layer_id).first() - if layer_obj is None: - return None - - update_fields = [] - if sync_to_geoserver is not None: - layer_obj.is_sync_to_geoserver = sync_to_geoserver - update_fields.append("is_sync_to_geoserver") - if is_stac_specs_generated is not None: - layer_obj.is_stac_specs_generated = is_stac_specs_generated - update_fields.append("is_stac_specs_generated") - - # `save(update_fields=...)` fires the post_save signal so the STAC - # auto-trigger handler in `computing.signals` can pick up the flip. - if update_fields: - layer_obj.save(update_fields=update_fields) - print( - f"Updated {update_fields} for layer ID: {layer_id} " - f"(sync={sync_to_geoserver}, stac={is_stac_specs_generated})" - ) - return layer_id - - except Exception as e: - print(f"Error updating layer sync status: {e}") - - -def _is_cache_valid(cache: dict, workspace: str) -> bool: - if workspace not in cache: - return False - age = time.time() - cache[workspace]["cached_at"] - if age > 3600: - logger.info(f"Cache expired for {workspace} (age: {int(age)}s)") - return False - return True - - -def _set_cache(cache: dict, workspace: str, data: set): - cache[workspace] = { - "data": data, - "cached_at": time.time(), - } - - -def send_report_email( - result, - report_type: str = "missing_layers", - recipients: list = None, -) -> bool: - """ - Generic reusable function to email a JSON report. - report_type: "missing_layers" or "missing_excel_files" - """ - if recipients is None: - recipients = getattr(settings, "MISSING_LAYER_RECIPIENTS", []) - - if isinstance(recipients, str): - recipients = [recipients] - - if not recipients: - logger.error("No recipients configured for report email.") - return False - - if report_type == "missing_layers": - subject = "Missing Layers Report" - attachment_name = "missing_layers.json" - - strict_result = result.get("Mandatory", {}) - can_be_empty_result = result.get("can_be_empty", {}) - - # Filter out workspaces with zero missing layers - strict_filtered = { - ws: data for ws, data in strict_result.items() if data.get("missing_layers") - } - can_be_empty_filtered = { - ws: data - for ws, data in can_be_empty_result.items() - if data.get("missing_layers") - } - - strict_summary = [] - total_strict_missing = 0 - for layer, data in strict_filtered.items(): - count = len(data.get("missing_layers", [])) - total_strict_missing += count - strict_summary.append(f"{layer}: {count}") - - can_be_empty_summary = [] - total_can_be_empty_missing = 0 - for layer, data in can_be_empty_filtered.items(): - count = len(data.get("missing_layers", [])) - total_can_be_empty_missing += count - can_be_empty_summary.append(f"{layer}: {count}") - - total_missing = total_strict_missing + total_can_be_empty_missing - - body = ( - "Missing Layers Report\n\n" - f"Total Missing: {total_missing}\n" - f" - Mandatory (needs attention): {total_strict_missing}\n" - f" - Can-Be-Empty (may be legitimately absent): {total_can_be_empty_missing}\n\n" - "---- Mandatory Workspaces (data expected everywhere) ----\n" - + ( - "\n".join(strict_summary) - if strict_summary - else "None — nothing missing" - ) - + "\n\n" - "---- Can-Be-Empty Workspaces (some locations may legitimately have no data) ----\n" - + ( - "\n".join(can_be_empty_summary) - if can_be_empty_summary - else "None — nothing missing" - ) - + "\n\nDetailed report attached." - ) - result = { - "Mandatory": strict_filtered, - "can_be_empty": can_be_empty_filtered, - } - - elif report_type == "missing_excel_files": - subject = "Missing Stats Excel/JSON Files Report" - attachment_name = "missing_excel_files.json" - total_locations = len(result) - total_missing_files = sum(len(loc.get("missing_files", [])) for loc in result) - total_xlsx_issues = sum(1 for loc in result if loc.get("xlsx_issues")) - body = ( - "Missing Stats Excel/JSON Files Report\n\n" - f"Tehsils which files(json/excel) are missing: {total_locations}\n" - f"Total missing files: {total_missing_files}\n" - f"Tehsils with xlsx sheet missing: {total_xlsx_issues}\n\n" - "Detailed report attached." - ) - - else: - logger.error(f"Unknown report_type: {report_type}") - return False - - attachment_content = json.dumps(result, indent=4) - max_retries = 3 - - for attempt in range(max_retries): - connection = None - try: - connection = get_connection(timeout=120) - connection.open() - email = EmailMessage( - subject=subject, - body=body, - from_email=settings.EMAIL_HOST_USER, - to=recipients, - connection=connection, - ) - email.attach( - attachment_name, - attachment_content, - "application/json", - ) - email.send() - logger.info(f"{subject} sent to {recipients}") - logger.info( - f"Attachment size: " - f"{len(attachment_content.encode('utf-8')) / 1024:.2f} KB" - ) - return True - except Exception as e: - logger.exception(f"Attempt {attempt + 1}/{max_retries} failed: {e}") - if attempt < max_retries - 1: - wait_time = 5 * (attempt + 1) - logger.info(f"Retrying after {wait_time} seconds...") - time.sleep(wait_time) - else: - logger.error("All attempts to send email failed.") - return False - finally: - if connection: - try: - connection.close() - except Exception: - pass - return False +import copy +import json +import logging +import os +import shutil +import time +import zipfile +from datetime import datetime, timedelta + +import ee +import fiona +import geopandas as gpd +import requests +from django.conf import settings +from django.core.mail import EmailMessage +from shapely.geometry import shape +from shapely.validation import explain_validity + +from computing.base_layer_setup import with_base_layers +from computing.models import Dataset, Layer +from geoadmin.models import ( + DistrictSOI, + State_Disritct_Block_Properties, + StateSOI, + TehsilSOI, +) +from projects.models import Project +from utilities.constants import ( + ADMIN_BOUNDARY_OUTPUT_DIR, + CATCHMENT_AREA, + GEE_ASSET_PATH, + GEE_HELPER_PATH, + GEE_PATHS, + SHAPEFILE_DIR, + STREAM_ORDER_ASSET, +) +from utilities.gee_utils import ( + check_task_status, + ee_initialize, + get_gee_asset_path, + get_gee_dir_path, + get_geojson_from_gcs, + is_asset_public, + is_gee_asset_exists, + sync_vector_to_gcs, + valid_gee_text, +) +from utilities.geoserver_utils import Geoserver +from django.core.mail import EmailMessage, get_connection +import time + +logger = logging.getLogger(__name__) + + +def generate_shape_files(path): + gdf = gpd.read_file(path + ".json") + if os.path.exists(path): + # Only replace the target shapefile directory. Removing the parent + # state/workspace directory here corrupts sibling outputs on reruns. + shutil.rmtree(path) + + os.makedirs(os.path.dirname(path), exist_ok=True) + gdf.to_file( + path, + driver="ESRI Shapefile", + ) + return path + + +def convert_to_zip(dir_name, file_type): + if file_type == "gpkg": + if dir_name.split(".")[-1] != "gpkg": + dir_name += ".gpkg" + with zipfile.ZipFile(dir_name + ".zip", "w", zipfile.ZIP_DEFLATED) as zipf: + zipf.write(dir_name, arcname=os.path.basename(dir_name)) + return dir_name + ".zip" + else: + return shutil.make_archive(dir_name, "zip", dir_name + "/") + + +def push_shape_to_geoserver( + path, store_name=None, workspace=None, layer_name=None, file_type="shp" +): + geo = Geoserver() + + print(f"layer_name: {layer_name}") + if layer_name: + try: + print(f"Attempting to delete store: {layer_name}") + geo.delete_vector_store(workspace=workspace, store=layer_name) + print(f"Successfully deleted store: {layer_name}") + except Exception as e: + print(f"Store does not exist or error deleting: {str(e)}") + + zip_path = convert_to_zip(path, file_type) + print(f"Zip path: {zip_path}") + print(f"Store name: {store_name}") + print(f"Workspace: {workspace}") + + response = geo.create_shp_datastore( + path=zip_path, + store_name=store_name, + workspace=workspace, + file_extension=file_type, + ) + print(f"Response: {response}") + return response + + +@with_base_layers("admin_boundary") +def kml_to_geojson(state_name, district_name, block_name, kml_path): + fiona.drvsupport.supported_drivers["kml"] = ( + "rw" # enable KML support which is disabled by default + ) + fiona.drvsupport.supported_drivers["KML"] = ( + "rw" # enable KML support which is disabled by default + ) + gdf = gpd.read_file(kml_path) + geometry_types = gdf.geometry.geometry.type.unique() + state_dir = os.path.join(ADMIN_BOUNDARY_OUTPUT_DIR, state_name) + + for gtype in geometry_types: + df = gdf.loc[gdf.geometry.geometry.type == gtype] + path = os.path.join(state_dir, f"{district_name}_{block_name}_{gtype}") + df.to_file(path + ".json", driver="GeoJSON") + generate_shape_files(path) + push_shape_to_geoserver(path, workspace="test_workspace") + + +def convert_kml_to_shapefile(kml_path, output_dir, shapefile_name): + if not os.path.exists(output_dir + "/" + shapefile_name): + os.makedirs(output_dir + "/" + shapefile_name) + + shapefile_path = os.path.join( + output_dir + "/" + shapefile_name, shapefile_name + ".shp" + ) + print("path path", shapefile_path) + cmd = f"ogr2ogr -f 'ESRI Shapefile' {shapefile_path} {kml_path}" # output.shp input.kml + os.system(command=cmd) + + return output_dir + "/" + shapefile_name + + +def kml_to_shp(state_name, district_name, block_name, kml_path): + shapefile_name = f"{district_name}_{block_name}" + shapefile_layer_path = convert_kml_to_shapefile( + kml_path, SHAPEFILE_DIR, shapefile_name + ) + + push_shape_to_geoserver(shapefile_layer_path, workspace="customkml") + + # os.remove(kml_path) + # shutil.rmtree(shapefile_layer_path) + os.remove(shapefile_layer_path + ".zip") + + +def sync_layer_to_geoserver(state_name, fc, layer_name, workspace): + state_dir = os.path.join("data/fc_to_shape", state_name) + if not os.path.exists(state_dir): + os.mkdir(state_dir) + path = os.path.join(state_dir, f"{layer_name}") + # Write the feature collection into json file + with open(path + ".json", "w") as f: + try: + f.write(f"{json.dumps(fc)}") + except Exception as e: + print(e) + + path = generate_shape_files(path) + return push_shape_to_geoserver(path, workspace=workspace, layer_name=layer_name) + + +def sync_fc_to_geoserver(fc, shp_folder, layer_name, workspace, style_name=None): + try: + geojson_fc = fc.getInfo() + except Exception as e: + print("Exception in getInfo()", e) + task_id = sync_vector_to_gcs(fc, layer_name, "GeoJSON") + check_task_status([task_id]) + + geojson_fc = get_geojson_from_gcs(layer_name) + geo = Geoserver() + if len(geojson_fc["features"]) > 0: + state_dir = os.path.join("data/fc_to_shape", shp_folder) + if not os.path.exists(state_dir): + os.mkdir(state_dir) + path = os.path.join(state_dir, f"{layer_name}") + + # Convert to GeoDataFrame + gdf = gpd.GeoDataFrame.from_features(geojson_fc["features"]) + + # Set CRS (Earth Engine uses EPSG:4326 by default) + gdf.crs = "EPSG:4326" + + gdf = fix_invalid_geometry_in_gdf(gdf) + + # Save as GeoPackage + gdf.to_file(path + ".gpkg", driver="GPKG") + res = push_shape_to_geoserver(path, workspace=workspace, file_type="gpkg") + if style_name: + style_res = geo.publish_style( + layer_name=layer_name, style_name=style_name, workspace=workspace + ) + print("Style response:", style_res) + return res + else: + return "No features in FeatureCollection" + + +def sync_project_fc_to_geoserver(fc, project_name, layer_name, workspace): + print("inside") + print(layer_name) + try: + geojson_fc = fc.getInfo() + except Exception as e: + print("Exception in getInfo()", e) + task_id = sync_vector_to_gcs(fc, layer_name, "GeoJSON") + check_task_status([task_id]) + + geojson_fc = get_geojson_from_gcs(layer_name) + print(len(geojson_fc["features"])) + if len(geojson_fc["features"]) > 0: + state_dir = os.path.join("data/fc_to_shape", project_name) + if not os.path.exists(state_dir): + os.mkdir(state_dir) + path = os.path.join(state_dir, f"{layer_name}") + + # Convert to GeoDataFrame + gdf = gpd.GeoDataFrame.from_features(geojson_fc["features"]) + + # Set CRS (Earth Engine uses EPSG:4326 by default) + gdf.crs = "EPSG:4326" + + gdf = fix_invalid_geometry_in_gdf(gdf) + + # Save as GeoPackage + gdf.to_file(path + ".gpkg", driver="GPKG") + print("pushed to geoserver") + return push_shape_to_geoserver( + path, workspace=workspace, layer_name=layer_name, file_type="gpkg" + ) + else: + print("no features found") + return + + +def to_camelcase(text): + words = text.split() + camelcase = words[0].lower() + for word in words[1:]: + camelcase += word.capitalize() + return camelcase + + +def create_chunk(aoi, description, chunk_size): + size = aoi.size().getInfo() + parts = size // chunk_size + # task_ids = [] + rois = [] + descs = [] + for part in range(parts + 1): + start = part * chunk_size + end = start + chunk_size + block_name_for_parts = description + "_" + str(start) + "-" + str(end) + roi = ee.FeatureCollection(aoi.toList(aoi.size()).slice(start, end)) + if roi.size().getInfo() > 0: + descs.append(block_name_for_parts) + rois.append(roi) + + return rois, descs + + +def merge_chunks( + aoi, + folder_list, + description, + chunk_size, + chunk_asset_path=GEE_HELPER_PATH, + merge_asset_path=GEE_ASSET_PATH, + merge_asset_id=None, +): + print("Merge Chunk task initiated") + ee_initialize() + size = aoi.size().getInfo() + parts = size // chunk_size + assets = [] + for part in range(parts + 1): + start = part * chunk_size + end = start + chunk_size + block_name_for_parts = description + "_" + str(start) + "-" + str(end) + src_asset_id = ( + get_gee_dir_path(folder_list, chunk_asset_path) + block_name_for_parts + ) + if is_gee_asset_exists(src_asset_id): + assets.append(ee.FeatureCollection(src_asset_id)) + + asset = ee.FeatureCollection(assets).flatten() + + asset_id = merge_asset_id or ( + get_gee_dir_path(folder_list, merge_asset_path) + description + ) + try: + # Export an ee.FeatureCollection as an Earth Engine asset. + task = ee.batch.Export.table.toAsset( + **{ + "collection": asset, + "description": description, + "assetId": asset_id, + } + ) + + task.start() + print("Successfully started the merge chunk", task.status()) + return task.status()["id"] + except Exception as e: + print(f"Error occurred in running merge task: {e}") + return None + + +def fix_invalid_geometry_in_gdf(gdf): + invalid = gdf[~gdf.is_valid] + if not invalid.empty: + print("Invalid geometries found:") + for idx, geom in invalid.geometry.items(): + print(f"Index {idx}: {explain_validity(geom)}") + gdf.loc[idx, "geometry"] = gdf.loc[idx, "geometry"].buffer(0) + + return gdf + + +def get_season_key(date): + """Return season key like 'rabi_2017-2018' based on Indian cropping seasons.""" + month = date.month + year = date.year + next_year = year + 1 + + if month in [1, 2]: + return f"rabi_{year - 1}-{year}" # Jan–Feb → Rabi of previous year + elif month in [11, 12]: + return f"rabi_{year}-{next_year}" # Nov–Dec → Rabi starting this year + elif month in [3, 4, 5, 6]: + return f"zaid_{year}-{next_year}" + elif month in [7, 8, 9, 10]: + return f"kharif_{year}-{next_year}" + else: + return None + + +def get_agri_year_key(season_key): + """Convert a season key to agricultural year key (e.g., rabi_2017-2018 → 2017-2018).""" + season, years = season_key.split("_") + start_year, end_year = map(int, years.split("-")) + + if season in ["kharif", "rabi"]: + return f"{start_year}-{end_year}" + elif season == "zaid": + return f"{start_year - 1}-{start_year}" # Zaid 2018-2019 → Agri year 2017-2018 + else: + return None + + +def calculate_precipitation_season( + geojson_filepath, draught_asset_id, start_year=None, end_year=None +): + if start_year is None or end_year is None: + raise ValueError( + "start_year and end_year are required for calculate_precipitation_season." + ) + start_year = int(start_year) + end_year = int(end_year) + + # Load the GeoJSON file + with open(geojson_filepath, "r") as f: + feature_collection = json.load(f) + + features_ee = [] + + for feature in feature_collection["features"]: + original_props = feature["properties"] + new_props = {} + + # Copy UID + if "uid" in original_props: + new_props["uid"] = original_props["uid"] + + agri_year_totals = {} + + # Parse precipitation date keys + for key, val in original_props.items(): + try: + date = datetime.strptime(key, "%Y-%m-%d") + season_key = get_season_key(date) + if not season_key: + continue + + agri_key = get_agri_year_key(season_key) + if not agri_key: + continue + + agri_start = int(agri_key.split("-")[0]) + if not (start_year <= agri_start <= end_year): + continue + + season = season_key.split("_")[0] # kharif, rabi etc + full_key = f"{season}_{agri_key}" + + agri_year_totals[full_key] = agri_year_totals.get(full_key, 0) + float( + val + ) + + except Exception: + continue + + # Add all seasonal totals to new_props + for agri_key, total in agri_year_totals.items(): + new_props[f"precipitation_{agri_key}"] = total + + # Create EE Feature + geom_ee = ee.Geometry(feature["geometry"]) + feature_ee = ee.Feature(geom_ee, new_props) + features_ee.append(feature_ee) + + # Left side FC + mws_fc = ee.FeatureCollection(features_ee) + + return mws_fc + + +def generate_geojson_with_ci_and_ndvi(zoi_asset, ci_asset, ndvi_asset, proj_id): + # Load project + proj_obj = Project.objects.get(pk=proj_id) + + # Build CI and NDVI asset paths + asset_path_ci = ( + get_gee_dir_path( + [proj_obj.name], asset_path=GEE_PATHS["WATER_REJ"]["GEE_ASSET_PATH"] + ) + + ci_asset + ) + + asset_path_ndvi = ( + get_gee_dir_path( + [proj_obj.name], asset_path=GEE_PATHS["WATER_REJ"]["GEE_ASSET_PATH"] + ) + + ndvi_asset + ) + + # Load FeatureCollections + zoi = ee.FeatureCollection(zoi_asset) + ci = ee.FeatureCollection(asset_path_ci) + ndvi = ee.FeatureCollection(asset_path_ndvi) + + # ------------------------- + # STEP 1: Join ZOI with Cropping Intensity + # ------------------------- + join = ee.Join.inner() + filter = ee.Filter.intersects(leftField=".geo", rightField=".geo") + zoi_ci_joined = join.apply(zoi, ci, filter) + + def merge_zoi_ci(pair): + zoi_feat = ee.Feature(pair.get("primary")) + ci_feat = ee.Feature(pair.get("secondary")) + merged_props = zoi_feat.toDictionary().combine(ci_feat.toDictionary(), True) + return ee.Feature(zoi_feat.geometry(), merged_props) + + zoi_with_ci = ee.FeatureCollection(zoi_ci_joined.map(merge_zoi_ci)) + + # ------------------------- + # STEP 2: Join ZOI+CI with NDVI + # ------------------------- + zoi_ndvi_joined = join.apply(zoi_with_ci, ndvi, filter) + + def merge_zoi_ci_ndvi(pair): + ci_feat = ee.Feature(pair.get("primary")) + ndvi_feat = ee.Feature(pair.get("secondary")) + merged_props = ci_feat.toDictionary().combine(ndvi_feat.toDictionary(), True) + return ee.Feature(ci_feat.geometry(), merged_props) + + final_merged = ee.FeatureCollection(zoi_ndvi_joined.map(merge_zoi_ci_ndvi)) + + # ------------------------- + # STEP 3: Export or Push to GeoServer + # ------------------------- + layer_name = f"WaterRejapp_zoi_{proj_obj.name}_{proj_obj.id}" + sync_project_fc_to_geoserver(final_merged, proj_obj.name, layer_name, "waterrej") + + +def get_directory_size(path): + total_size = 0 + for dirpath, dirnames, filenames in os.walk(path): + for filename in filenames: + file_path = os.path.join(dirpath, filename) + if os.path.isfile(file_path): + total_size += os.path.getsize(file_path) + return total_size + + +def generate_geojson_with_ci_ndvi_ndmi( + zoi_asset, ci_asset, ndvi_asset, ndmi_asset, proj_id +): + + # Load project + proj_obj = Project.objects.get(pk=proj_id) + + zoi = ee.FeatureCollection(zoi_asset) + print("Number of features zoi:", zoi.size().getInfo()) + + ci = ee.FeatureCollection(ci_asset) + print("Number of features zoi:", ci.size().getInfo()) + ndvi = ee.FeatureCollection(ndmi_asset) + print("Number of features zoi:", ndvi.size().getInfo()) + ndmi = ee.FeatureCollection(ndmi_asset) + print("Number of features zoi:", ndmi.size().getInfo()) + + # ------------------------- + # STEP 1: Join ZOI with CI + # ------------------------- + join = ee.Join.inner() + filter = ee.Filter.intersects(leftField=".geo", rightField=".geo") + zoi_ci_joined = join.apply(zoi, ci, filter) + + def merge_zoi_ci(pair): + zoi_feat = ee.Feature(pair.get("primary")) + ci_feat = ee.Feature(pair.get("secondary")) + merged_props = zoi_feat.toDictionary().combine(ci_feat.toDictionary(), True) + return ee.Feature(zoi_feat.geometry(), merged_props) # ✅ keep ZOI geom + + zoi_with_ci = ee.FeatureCollection(zoi_ci_joined.map(merge_zoi_ci)) + + # ------------------------- + # STEP 2: Join with NDVI + # ------------------------- + zoi_ndvi_joined = join.apply(zoi_with_ci, ndvi, filter) + + def merge_zoi_ci_ndvi(pair): + prev_feat = ee.Feature(pair.get("primary")) + ndvi_feat = ee.Feature(pair.get("secondary")) + merged_props = prev_feat.toDictionary().combine(ndvi_feat.toDictionary(), True) + return ee.Feature(prev_feat.geometry(), merged_props) # ✅ still ZOI geom + + zoi_ci_ndvi = ee.FeatureCollection(zoi_ndvi_joined.map(merge_zoi_ci_ndvi)) + + # ------------------------- + # STEP 3: Join with NDMI + # ------------------------- + zoi_ndmi_joined = join.apply(zoi_ci_ndvi, ndmi, filter) + + def merge_zoi_ci_ndvi_ndmi(pair): + prev_feat = ee.Feature(pair.get("primary")) + ndmi_feat = ee.Feature(pair.get("secondary")) + merged_props = prev_feat.toDictionary().combine(ndmi_feat.toDictionary(), True) + return ee.Feature(prev_feat.geometry(), merged_props) # ✅ keep ZOI geom + + final_merged = ee.FeatureCollection(zoi_ndmi_joined.map(merge_zoi_ci_ndvi_ndmi)) + + # ------------------------- + # STEP 4: Export or Push to GeoServer + # ------------------------- + layer_name = f"WaterRejapp_zoi_{proj_obj.name}_{proj_obj.id}" + print(layer_name) + sync_project_fc_to_geoserver(final_merged, proj_obj.name, layer_name, "waterrej") + + +def generate_geojson_with_ci_ndvi(zoi_asset, ci_asset, ndvi_asset, proj_id): + # Load project + proj_obj = Project.objects.get(pk=proj_id) + + # Initialize Earth Engine + ee_initialize(4) + + # Load FeatureCollections + zoi = ee.FeatureCollection(zoi_asset) + ci = ee.FeatureCollection(ci_asset) + ndvi = ee.FeatureCollection(ndvi_asset) + + print("ZOI:", zoi.size().getInfo()) + print("CI:", ci.size().getInfo()) + print("NDVI:", ndvi.size().getInfo()) + + # Common join logic on UID + join = ee.Join.inner() + uid_filter = ee.Filter.equals(leftField="UID", rightField="UID") + + # --- Join ZOI + CI --- + zoi_ci_joined = join.apply(zoi, ci, uid_filter) + + def merge_zoi_ci(pair): + zoi_feat = ee.Feature(pair.get("primary")) + ci_feat = ee.Feature(pair.get("secondary")) + merged_props = zoi_feat.toDictionary().combine(ci_feat.toDictionary(), True) + # Keep ZOI geometry only + return ee.Feature(zoi_feat.geometry(), merged_props) + + zoi_with_ci = ee.FeatureCollection(zoi_ci_joined.map(merge_zoi_ci)) + + # --- Join with NDVI --- + zoi_ndvi_joined = join.apply(zoi_with_ci, ndvi, uid_filter) + + def merge_with_ndvi(pair): + base_feat = ee.Feature(pair.get("primary")) + ndvi_feat = ee.Feature(pair.get("secondary")) + merged_props = base_feat.toDictionary().combine(ndvi_feat.toDictionary(), True) + # Always retain ZOI geometry + return ee.Feature(base_feat.geometry(), merged_props) + + merged_final = ee.FeatureCollection(zoi_ndvi_joined.map(merge_with_ndvi)) + + # --- Ensure ZOI geometry retained in all features --- + merged_final = merged_final.map( + lambda f: ee.Feature( + f.setGeometry( + ee.Feature( + zoi.filter(ee.Filter.eq("UID", f.get("UID"))).first() + ).geometry() + ) + ) + ) + + layer_name = f"WaterRejapp_zoi_{proj_obj.name}_{proj_obj.id}" + print(layer_name) + + sync_project_fc_to_geoserver(merged_final, proj_obj.name, layer_name, "waterrej") + + +def _get_prod_backend_url(): + return getattr(settings, "PROD_BACKEND_URL", "").rstrip("/") + + +def _get_prod_api_key(): + return getattr(settings, "PROD_BACKEND_API_KEY", "") + + +def _sync_layer_to_prod_db(payload: dict): + prod_url = _get_prod_backend_url() + if not prod_url: + return None + + endpoint = prod_url + "/api/v1/sync_layer_remote/" + try: + response = requests.post( + endpoint, + json=payload, + headers={"X-Api-Key": _get_prod_api_key()}, + timeout=30, + ) + if response.status_code not in (200, 201): + logger.warning( + "Prod DB sync returned %s for layer %s: %s", + response.status_code, + payload.get("layer_name"), + response.text, + ) + return None + layer_id = response.json().get("layer_id") + logger.info( + "Layer %s synced to prod DB (id=%s).", payload.get("layer_name"), layer_id + ) + return layer_id + except requests.RequestException as e: + logger.error( + "Failed to sync layer %s to prod DB: %s", payload.get("layer_name"), e + ) + return None + + +def _update_layer_sync_remote( + layer_id, sync_to_geoserver=None, is_stac_specs_generated=None +): + prod_url = _get_prod_backend_url() + if not prod_url or layer_id is None: + return + + endpoint = prod_url + "/api/v1/update_layer_sync_remote/" + payload = { + "layer_id": layer_id, + "sync_to_geoserver": sync_to_geoserver, + "is_stac_specs_generated": is_stac_specs_generated, + } + try: + response = requests.post( + endpoint, + json=payload, + headers={"X-Api-Key": _get_prod_api_key()}, + timeout=30, + ) + if response.status_code not in (200, 201): + logger.warning( + "Prod layer sync status update returned %s for layer %s: %s", + response.status_code, + layer_id, + response.text, + ) + else: + logger.info("Layer sync status updated on prod DB for id=%s.", layer_id) + except requests.RequestException as e: + logger.error( + "Failed to update layer sync status on prod DB for id=%s: %s", layer_id, e + ) + + + +def get_existing_end_year(dataset_name, layer_name): + """fetch objects from db on the basis of dataset name and layer_name""" + dataset = Dataset.objects.get(name=dataset_name) + layer_obj = Layer.objects.get(dataset=dataset, layer_name=layer_name) + existing_end_date = layer_obj.misc["end_year"] + print("existing_end_date", existing_end_date) + return existing_end_date + + +def get_layer_object(state, district, block, layer_name, dataset_name): + state_obj = StateSOI.objects.get(state_name__iexact=state) + district_obj = DistrictSOI.objects.get( + district_name__iexact=district, state=state_obj + ) + block_obj = TehsilSOI.objects.get(tehsil_name__iexact=block, district=district_obj) + layer_obj = ( + Layer.objects.filter( + state=state_obj, + district=district_obj, + block=block_obj, + layer_name=layer_name, + dataset__name=dataset_name, + ) + .order_by("-layer_version") + .first() + ) + return layer_obj + + +def update_dashboard_geojson( + state=None, + district=None, + block=None, + layer_name=None, + workspace_name=None, + proj_id=None, +): + if state and block and block: + print(f"🔄 Updating GeoJSON for {state}, {district}, {block}") + + # Get related objects + state_obj = StateSOI.objects.get(state_name=state) + district_obj = DistrictSOI.objects.get(district_name=district) + tehsil_obj = TehsilSOI.objects.get(tehsil_name=block) # fixed typo + + # Get or create main record + obj, created = State_Disritct_Block_Properties.objects.get_or_create( + state=state_obj, district=district_obj, tehsil=tehsil_obj + ) + else: + obj = Project.objects.get(pk=proj_id) + + # Map suffix to json_key + suffix_to_key = { + "wb": "wb_geojson", + "zoi": "zoi_geojson", + "mws": "mws_geojson", + } + + # Detect which key this layer corresponds to + json_key = None + for suffix, key in suffix_to_key.items(): + if layer_name == f"{state}_{district}_{block}_{suffix}": + json_key = key + break + + if not json_key: + print(f"⚠️ Layer name {layer_name} did not match any known type.") + return + + # Construct GeoServer URL + waterrej_url = ( + f"https://geoserver.core-stack.org:8443/geoserver/waterrej/ows?" + f"service=WFS&version=1.0.0&request=GetFeature&typeName={workspace_name}:{layer_name}" + f"&outputFormat=application%2Fjson" + ) + + # Load existing dashboard_geojson or create new + if proj_id: + misc = obj.dashboard_geojson or {} + else: + misc = obj.geojson_path or {} + + # Ensure waterrej section exists + if "waterrej" not in misc: + misc["waterrej"] = {} + + # Update or add this specific json_key + misc["waterrej"][json_key] = waterrej_url + + # Save the updated JSON field + obj.dashboard_geojson = misc + obj.save() + + print(f"✅ Added/Updated {json_key} for {state}, {district}, {block}") + + +def clean_geometry(geom): + """ + Clean geometry: + - Dissolve multipolygon → single polygon + - Remove holes automatically + - Fix invalid topology + - Buffer tiny polygons + """ + + # 1. Dissolve multi-polygons and remove holes + geom = geom.dissolve(maxError=1) + + # 2. Fix invalid rings by simplifying slightly (NEVER buffer(0)) + geom = geom.simplify(1) + + # 3. Buffer polygons smaller than 1 pixel (< 900 m²) + area = geom.area() + geom = ee.Algorithms.If( + area.lt(900), + geom.buffer(15), + geom, # ensure raster pixel center is captured + ) + + return ee.Geometry(geom) + + +def safe_reduce_max(image, geom, scale=30): + geom = clean_geometry(geom) + + val = ( + image.unmask(0) + .reduceRegion( + reducer=ee.Reducer.max(), + geometry=geom, + scale=scale, + maxPixels=1e13, + tileScale=4, + bestEffort=True, + ) + .get("b1") + ) + + return ee.Number(ee.Algorithms.If(val, val, 0)) + + +# ------------------------------------------------------ +# SAFE REDUCE MAX FUNCTION +# ------------------------------------------------------ +def safe_reduce_max(image, geom, scale=30): + geom = clean_geometry(geom) + + result = ( + image.unmask(0) + .reduceRegion( + reducer=ee.Reducer.max(), + geometry=geom, + scale=scale, + maxPixels=1e13, + tileScale=4, + bestEffort=True, + ) + .get("b1") + ) + + # Convert null → 0 + return ee.Number(ee.Algorithms.If(result, result, 0)) + + +# ------------------------------------------------------ +# MAIN FUNCTION TO PROCESS SWB LAYER +# ------------------------------------------------------ +def generate_swb_layer_with_max_so_catchment( + roi=None, + gee_account_id=None, + stream_order_asset_id=STREAM_ORDER_ASSET, + catchment_area_asset_id=CATCHMENT_AREA, +): + ee_initialize(gee_account_id) + + stream_order_band = ee.Image(stream_order_asset_id).select([0], ["b1"]) + catchment_band = ee.Image(catchment_area_asset_id).select([0], ["b1"]) + + # Processing per waterbody + def compute_for_feature(feature): + geom = feature.geometry() + + max_so = safe_reduce_max(stream_order_band, geom, scale=30) + max_ca = safe_reduce_max(catchment_band, geom, scale=30) + + return feature.set( + { + "max_stream_order": max_so, + "max_catchment_area": max_ca, + } + ) + + # Map over the feature collection + return roi.map(compute_for_feature) + + + + +def save_layer_info_to_db( + state, + district, + block, + layer_name, + asset_id, + dataset_name, + sync_to_geoserver=False, + layer_version="1.0", + algorithm=None, + algorithm_version="1.0", + misc=None, + is_override=False, +): + if _get_prod_backend_url(): + layer_id = _sync_layer_to_prod_db( + { + "state": state, + "district": district, + "block": block, + "layer_name": layer_name, + "asset_id": asset_id, + "dataset_name": dataset_name, + "sync_to_geoserver": sync_to_geoserver, + "layer_version": layer_version, + "algorithm": algorithm, + "algorithm_version": algorithm_version, + "misc": misc, + "is_override": is_override, + } + ) + if layer_id is not None: + return layer_id + logger.warning( + "Prod DB sync failed for layer %s — falling back to local DB write.", + layer_name, + ) + + print("inside the save_layer_info_to_db function") + + dataset = Dataset.objects.get(name=dataset_name) + + try: + state_obj = StateSOI.objects.get(state_name__iexact=state) + district_obj = DistrictSOI.objects.get( + district_name__iexact=district, state=state_obj + ) + block_obj = TehsilSOI.objects.get( + tehsil_name__iexact=block, district=district_obj + ) + except Exception as e: + print("Error fetching in state district block:", e) + return + + is_public = is_asset_public(asset_id) + + # Check if there’s an existing layer + existing_layer = ( + Layer.objects.filter( + dataset=dataset, + layer_name=layer_name, + state=state_obj, + district=district_obj, + block=block_obj, + ) + .order_by("-layer_version") + .first() + ) + + if existing_layer: + if existing_layer.algorithm_version != algorithm_version: + # Algorithm version changed --> create new record with incremented layer_version + new_layer_version = str(float(existing_layer.layer_version) + 1) + print( + f"Algorithm version changed. Creating new layer version: {new_layer_version}" + ) + layer_obj = Layer.objects.create( + dataset=dataset, + layer_name=layer_name, + state=state_obj, + district=district_obj, + block=block_obj, + layer_version=new_layer_version, + algorithm=algorithm, + algorithm_version=algorithm_version, + is_sync_to_geoserver=sync_to_geoserver, + is_public_gee_asset=is_public, + is_override=is_override, + misc=misc, + gee_asset_path=asset_id, + ) + else: + # Algorithm version is same --> update existing layer + print("Algorithm version same. Updating existing layer.") + _deprecated_misc_keys = {"is_computed_locally"} + base_misc = { + k: v + for k, v in (existing_layer.misc or {}).items() + if k not in _deprecated_misc_keys + } + merged_misc = {**base_misc, **(misc or {})} + for field, value in { + "algorithm": algorithm, + "algorithm_version": algorithm_version, + "is_sync_to_geoserver": sync_to_geoserver, + "is_public_gee_asset": is_public, + "is_override": is_override, + "misc": merged_misc, + "gee_asset_path": asset_id, + }.items(): + setattr(existing_layer, field, value) + existing_layer.save() + layer_obj = existing_layer + else: + # No existing record --> create a new one + print("No existing layer found. Creating new one.") + layer_obj = Layer.objects.create( + dataset=dataset, + layer_name=layer_name, + state=state_obj, + district=district_obj, + block=block_obj, + layer_version=layer_version, + algorithm=algorithm, + algorithm_version=algorithm_version, + is_sync_to_geoserver=sync_to_geoserver, + is_public_gee_asset=is_public, + is_override=is_override, + misc=misc, + gee_asset_path=asset_id, + ) + + print(f"Saved layer info (id={layer_obj.id}, version={layer_obj.layer_version})") + return layer_obj.id + + +def update_layer_sync_status( + layer_id, sync_to_geoserver=None, is_stac_specs_generated=None +): + if _get_prod_backend_url(): + _update_layer_sync_remote( + layer_id, + sync_to_geoserver=sync_to_geoserver, + is_stac_specs_generated=is_stac_specs_generated, + ) + if sync_to_geoserver: + try: + from utilities.layer_generation_mode import record_sync_layer_id + + record_sync_layer_id(layer_id) + except Exception: + pass + return layer_id + + try: + layer_obj = Layer.objects.filter(id=layer_id).first() + if layer_obj is None: + return None + + update_fields = [] + if sync_to_geoserver is not None: + layer_obj.is_sync_to_geoserver = sync_to_geoserver + update_fields.append("is_sync_to_geoserver") + if is_stac_specs_generated is not None: + layer_obj.is_stac_specs_generated = is_stac_specs_generated + update_fields.append("is_stac_specs_generated") + + # `save(update_fields=...)` fires the post_save signal so the STAC + # auto-trigger handler in `computing.signals` can pick up the flip. + if update_fields: + layer_obj.save(update_fields=update_fields) + print( + f"Updated {update_fields} for layer ID: {layer_id} " + f"(sync={sync_to_geoserver}, stac={is_stac_specs_generated})" + ) + if sync_to_geoserver: + try: + from utilities.layer_generation_mode import record_sync_layer_id + + record_sync_layer_id(layer_id) + except Exception: + pass + return layer_id + + except Exception as e: + print(f"Error updating layer sync status: {e}") + + +def _is_cache_valid(cache: dict, workspace: str) -> bool: + if workspace not in cache: + return False + age = time.time() - cache[workspace]["cached_at"] + if age > 3600: + logger.info(f"Cache expired for {workspace} (age: {int(age)}s)") + return False + return True + + +def _set_cache(cache: dict, workspace: str, data: set): + cache[workspace] = { + "data": data, + "cached_at": time.time(), + } + + +def send_report_email( + result, + report_type: str = "missing_layers", + recipients: list = None, +) -> bool: + """ + Generic reusable function to email a JSON report. + report_type: "missing_layers" or "missing_excel_files" + """ + if recipients is None: + recipients = getattr(settings, "MISSING_LAYER_RECIPIENTS", []) + + if isinstance(recipients, str): + recipients = [recipients] + + if not recipients: + logger.error("No recipients configured for report email.") + return False + + if report_type == "missing_layers": + subject = "Missing Layers Report" + attachment_name = "missing_layers.json" + + strict_result = result.get("Mandatory", {}) + can_be_empty_result = result.get("can_be_empty", {}) + + # Filter out workspaces with zero missing layers + strict_filtered = { + ws: data for ws, data in strict_result.items() if data.get("missing_layers") + } + can_be_empty_filtered = { + ws: data + for ws, data in can_be_empty_result.items() + if data.get("missing_layers") + } + + strict_summary = [] + total_strict_missing = 0 + for layer, data in strict_filtered.items(): + count = len(data.get("missing_layers", [])) + total_strict_missing += count + strict_summary.append(f"{layer}: {count}") + + can_be_empty_summary = [] + total_can_be_empty_missing = 0 + for layer, data in can_be_empty_filtered.items(): + count = len(data.get("missing_layers", [])) + total_can_be_empty_missing += count + can_be_empty_summary.append(f"{layer}: {count}") + + total_missing = total_strict_missing + total_can_be_empty_missing + + body = ( + "Missing Layers Report\n\n" + f"Total Missing: {total_missing}\n" + f" - Mandatory (needs attention): {total_strict_missing}\n" + f" - Can-Be-Empty (may be legitimately absent): {total_can_be_empty_missing}\n\n" + "---- Mandatory Workspaces (data expected everywhere) ----\n" + + ( + "\n".join(strict_summary) + if strict_summary + else "None — nothing missing" + ) + + "\n\n" + "---- Can-Be-Empty Workspaces (some locations may legitimately have no data) ----\n" + + ( + "\n".join(can_be_empty_summary) + if can_be_empty_summary + else "None — nothing missing" + ) + + "\n\nDetailed report attached." + ) + result = { + "Mandatory": strict_filtered, + "can_be_empty": can_be_empty_filtered, + } + + elif report_type == "missing_excel_files": + subject = "Missing Stats Excel/JSON Files Report" + attachment_name = "missing_excel_files.json" + total_locations = len(result) + total_missing_files = sum(len(loc.get("missing_files", [])) for loc in result) + total_xlsx_issues = sum(1 for loc in result if loc.get("xlsx_issues")) + body = ( + "Missing Stats Excel/JSON Files Report\n\n" + f"Tehsils which files(json/excel) are missing: {total_locations}\n" + f"Total missing files: {total_missing_files}\n" + f"Tehsils with xlsx sheet missing: {total_xlsx_issues}\n\n" + "Detailed report attached." + ) + + else: + logger.error(f"Unknown report_type: {report_type}") + return False + + attachment_content = json.dumps(result, indent=4) + max_retries = 3 + + for attempt in range(max_retries): + connection = None + try: + connection = get_connection(timeout=120) + connection.open() + email = EmailMessage( + subject=subject, + body=body, + from_email=settings.EMAIL_HOST_USER, + to=recipients, + connection=connection, + ) + email.attach( + attachment_name, + attachment_content, + "application/json", + ) + email.send() + logger.info(f"{subject} sent to {recipients}") + logger.info( + f"Attachment size: " + f"{len(attachment_content.encode('utf-8')) / 1024:.2f} KB" + ) + return True + except Exception as e: + logger.exception(f"Attempt {attempt + 1}/{max_retries} failed: {e}") + if attempt < max_retries - 1: + wait_time = 5 * (attempt + 1) + logger.info(f"Retrying after {wait_time} seconds...") + time.sleep(wait_time) + else: + logger.error("All attempts to send email failed.") + return False + finally: + if connection: + try: + connection.close() + except Exception: + pass + return False diff --git a/docs/guide/api/api.md b/docs/guide/api/api.md index 95d766d7..2d98561d 100644 --- a/docs/guide/api/api.md +++ b/docs/guide/api/api.md @@ -2,6 +2,9 @@ This document provides a comprehensive overview of the Core Stack Backend API endpoints, their functionality, and usage. +For local layer-generation endpoints, payloads, and curl examples, see +[Local Compute Pipeline APIs](local_compute_pipeline.md). + ## Authentication Endpoints ### Authentication Flow diff --git a/dpr/api.py b/dpr/api.py index 5ecce98f..7eb56fff 100644 --- a/dpr/api.py +++ b/dpr/api.py @@ -562,9 +562,8 @@ def download_report(request): @api_view(["GET"]) -@auth_free @schema(None) -@api_security_check(auth_type="Auth_free") +@auth_free def generate_tehsil_report(request): try: # ? district, block, mwsId diff --git a/nrm_app/celery.py b/nrm_app/celery.py index 397563c1..e1c58af2 100755 --- a/nrm_app/celery.py +++ b/nrm_app/celery.py @@ -16,3 +16,6 @@ # Load task modules from all registered Django app configs. app.autodiscover_tasks(INSTALLED_APPS) + +# Register Celery task logging signals (prerun / postrun / failure). +import computing.celery_task_logging # noqa: E402, F401 diff --git a/nrm_app/settings.py b/nrm_app/settings.py index e7ec179a..f714ca8b 100755 --- a/nrm_app/settings.py +++ b/nrm_app/settings.py @@ -11,6 +11,7 @@ """ import os +import site from datetime import timedelta from pathlib import Path @@ -25,6 +26,18 @@ BASE_DIR = Path(__file__).resolve().parent.parent os.environ.setdefault("BACKEND_DIR", str(BASE_DIR)) +PYTHON_ENV_DIR = Path(site.__file__).resolve() +if "lib" in PYTHON_ENV_DIR.parts: + PYTHON_ENV_DIR = Path(*PYTHON_ENV_DIR.parts[: PYTHON_ENV_DIR.parts.index("lib")]) + GDAL_DATA_DIR = PYTHON_ENV_DIR / "share" / "gdal" + PROJ_DATA_DIR = PYTHON_ENV_DIR / "share" / "proj" + + if GDAL_DATA_DIR.is_dir(): + os.environ.setdefault("GDAL_DATA", str(GDAL_DATA_DIR)) + if PROJ_DATA_DIR.is_dir(): + os.environ.setdefault("PROJ_LIB", str(PROJ_DATA_DIR)) + os.environ.setdefault("PROJ_DATA", str(PROJ_DATA_DIR)) + def resolve_env_path(name, default="", *, trailing_sep=False): raw_value = str(os.environ.get(name, default) or "").strip() @@ -74,7 +87,7 @@ def resolve_env_path(name, default="", *, trailing_sep=False): USERNAME_GESDISC = env("USERNAME_GESDISC") PASSWORD_GESDISC = env("PASSWORD_GESDISC") -STATIC_ROOT = "static/" +STATIC_ROOT = BASE_DIR / "static" GEE_HELPER_ACCOUNT_ID = env("GEE_HELPER_ACCOUNT_ID") GEE_DEFAULT_ACCOUNT_ID = env("GEE_DEFAULT_ACCOUNT_ID") ADMIN_GROUP_ID = env("ADMIN_GROUP_ID") @@ -269,13 +282,17 @@ def resolve_env_path(name, default="", *, trailing_sep=False): # Celery CELERY_TIMEZONE = "Asia/Kolkata" CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" +LAYER_GENERATION_SYNC_MODE = env.bool("LAYER_GENERATION_SYNC_MODE", default=False) +SYNC_LAYER = env.bool("SYNC_LAYER", default=False) +STAC_UPLOAD_TO_S3 = env.bool("STAC_UPLOAD_TO_S3", default=False) +STAC_OVERWRITE_METADATA = env.bool("STAC_OVERWRITE_METADATA", default=True) # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ AUTH_USER_MODEL = "users.User" -STATIC_URL = "static/" -STATIC_ROOT = "static/" +STATIC_URL = "/static/" +STATIC_ROOT = BASE_DIR / "static" ASSET_DIR = "/home/ubuntu/cfpt/core-stack-backend/assets/" # Media files (User uploaded content) @@ -329,6 +346,11 @@ def resolve_env_path(name, default="", *, trailing_sep=False): "level": "DEBUG", "propagate": False, }, + "core_stack.layer_generation": { + "handlers": ["console"], + "level": "INFO", + "propagate": False, + }, }, } @@ -359,6 +381,13 @@ def resolve_env_path(name, default="", *, trailing_sep=False): PROD_BACKEND_URL = env("PROD_BACKEND_URL", default="") PROD_BACKEND_API_KEY = env("PROD_BACKEND_API_KEY", default="") +PROD_GEOSERVER_URL = env("PROD_GEOSERVER_URL", default="") +PROD_GEOSERVER_USERNAME = env("PROD_GEOSERVER_USERNAME", default="") +PROD_GEOSERVER_PASSWORD = env("PROD_GEOSERVER_PASSWORD", default="") + +PROD_BACKEND_URL = env("PROD_BACKEND_URL", default="") +PROD_BACKEND_API_KEY = env("PROD_BACKEND_API_KEY", default="") + CE_BUCKET_URL = env("CE_BUCKET_URL") EARTH_DATA_USER = env("EARTH_DATA_USER") diff --git a/nrm_app/wsgi.py b/nrm_app/wsgi.py index 8d89c4c9..f2cc7948 100755 --- a/nrm_app/wsgi.py +++ b/nrm_app/wsgi.py @@ -21,13 +21,20 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "nrm_app.settings") +conda_env = os.path.dirname(site.__file__).split("/lib")[0] +gdal_data = os.path.join(conda_env, "share", "gdal") +proj_lib = os.path.join(conda_env, "share", "proj") +lib_path = os.path.join(conda_env, "lib") + +if os.path.isdir(gdal_data): + os.environ["GDAL_DATA"] = gdal_data +if os.path.isdir(proj_lib): + os.environ["PROJ_LIB"] = proj_lib + os.environ["PROJ_DATA"] = proj_lib +if os.path.isdir(lib_path): + os.environ["LD_LIBRARY_PATH"] = lib_path + if DEBUG: - conda_env = os.path.dirname(site.__file__).split('/lib')[0] print("CONDA ENV: ", conda_env) - os.environ['GDAL_DATA'] = f"{conda_env}/share/gdal" - os.environ['LD_LIBRARY_PATH'] = f"{conda_env}/lib" -else: - os.environ['GDAL_DATA'] = '/home/ubuntu/prod_dir/nrm-app/venv/envs/corestack/share/gdal' - os.environ['LD_LIBRARY_PATH'] = '/home/ubuntu/prod_dir/nrm-app/venv/envs/corestack/lib' application = get_wsgi_application() diff --git a/utilities/constants.py b/utilities/constants.py index 9e702be8..fd15762e 100644 --- a/utilities/constants.py +++ b/utilities/constants.py @@ -5,14 +5,14 @@ ADMIN_BOUNDARY_INPUT_DIR = "data/admin-boundary/input" ADMIN_BOUNDARY_OUTPUT_DIR = "data/admin-boundary/output" -# Canonical local pipeline resources. Runtime pipelines read these defaults -# directly; their YAML `sources` entries are optional test overrides. -BASE_RESOURCES_DIR = "data/base_resources" -ADMIN_BOUNDARY_GPKG = f"{BASE_RESOURCES_DIR}/cs_admin_standard.gpkg" -FACILITIES_GPKG = f"{BASE_RESOURCES_DIR}/cs_pan_india_facilities.gpkg" -ANTYODAYA_2020_CSV = f"{BASE_RESOURCES_DIR}/cs_antyodaya_2020_cluster_analysis.csv" -LIVESTOCK_CENSUS_20_CSV = f"{BASE_RESOURCES_DIR}/cs_livestock_census_20.csv" - +# Canonical local pipeline resources. Runtime pipelines read these defaults +# directly; their YAML `sources` entries are optional test overrides. +BASE_RESOURCES_DIR = "data/base_resources" +ADMIN_BOUNDARY_GPKG = f"{BASE_RESOURCES_DIR}/cs_admin_standard.gpkg" +FACILITIES_GPKG = f"{BASE_RESOURCES_DIR}/cs_pan_india_facilities.gpkg" +ANTYODAYA_2020_CSV = f"{BASE_RESOURCES_DIR}/cs_antyodaya_2020_cluster_analysis.csv" +LIVESTOCK_CENSUS_20_CSV = f"{BASE_RESOURCES_DIR}/cs_livestock_census_20.csv" + NREGA_ASSETS_INPUT_DIR = "data/nrega_assets/input" NREGA_ASSETS_OUTPUT_DIR = "data/nrega_assets/output" @@ -344,7 +344,7 @@ # other FIRST_COMPUTING_API_PATH = "/api/v1/generate_block_layer/" -WBC = "projects/ext-datasets/assets/datasets/WBC_" +WBC = "projects/ext-datasets/assets/datasets/WBC/WBC_" WATERREJUVENATION_PROJECT = GEE_STORAGE_PROJECT # Plantation @@ -370,7 +370,10 @@ CCD_RASTER = "projects/corestack-trees/assets/tree_characteristics/modal_ccd_" CH_RASTER = "projects/corestack-trees/assets/tree_characteristics/modal_ch_" TREE_OVERALL_CHANGE = ( - "projects/corestack-trees/assets/tree_characteristics/overall_change_2017_2022" + "projects/corestack-trees/assets/tree_characteristics/overall_change_2017_2023" +) +LTP_STP_CHANGE = ( + "projects/corestack-trees/assets/tree_characteristics/ltp_stp_change_2017_2024" ) CANAL_PAN_INDIA_ASSET = "projects/ext-datasets/assets/datasets/Canal_pan_india" diff --git a/utilities/download_gpkg_from_geoserver.py b/utilities/download_gpkg_from_geoserver.py new file mode 100644 index 00000000..79a8ee3f --- /dev/null +++ b/utilities/download_gpkg_from_geoserver.py @@ -0,0 +1,126 @@ +import requests +import geopandas as gpd +from pathlib import Path + +from nrm_app import settings +from utilities.gee_utils import valid_gee_text +from computing.config_loader import PROJECT_ROOT + +""" +from utilities.download_gpkg_from_geoserver import generate_gpkg +generate_gpkg(state="Odisha", district="Srikakulam", block="Bhamini", workspace="panchayat_boundaries") +""" + +BASE_OUTPUT_DIR = PROJECT_ROOT / "data/base_layers/" + + +def build_layer_and_path(state, district, block, workspace): + """ + Create: + - sanitized names + - geoserver layer name + - gpkg output path + + Example Output: + mws_gpkg/odisha/srikakulam/bhamini.gpkg + """ + + state = valid_gee_text(state.lower()) + district = valid_gee_text(district.lower()) + block = valid_gee_text(block.lower()) + + # GeoServer layer + output_base = BASE_OUTPUT_DIR + + if workspace == "mws": + layer_name = f"mws:mws_{district}_{block}" + output_base = output_base / "tehsil_watersheds" + elif workspace == "panchayat_boundaries": + layer_name = f"panchayat_boundaries:{district}_{block}" + output_base = output_base / "village_boundaries" + + output_dir = output_base / state / district + output_dir.mkdir(parents=True, exist_ok=True) + gpkg_path = output_dir / f"{block}.gpkg" + + return layer_name, gpkg_path, district, block + + +def read_layer_from_geoserver(layer_name): + """ + Read GeoServer WFS layer directly into GeoDataFrame. + """ + + params = { + "service": "WFS", + "version": "2.0.0", + "request": "GetFeature", + "typeName": layer_name, + "outputFormat": "application/json", + "srsName": "EPSG:4326", + } + + geoserver_url = settings.PROD_GEOSERVER_URL.rstrip("/") + wfs_url = f"{geoserver_url}/wfs" + + # Some workspaces might be protected, use auth if available + auth = None + if hasattr(settings, "GEOSERVER_USER") and hasattr(settings, "GEOSERVER_PASSWORD"): + auth = (settings.GEOSERVER_USER, settings.GEOSERVER_PASSWORD) + + response = requests.get( + wfs_url, + params=params, + auth=auth, + timeout=120, + verify=False, + ) + + response.raise_for_status() + + # Verify we actually got JSON and not an HTML redirect page + if "text/html" in response.headers.get("Content-Type", ""): + raise ValueError( + f"GeoServer returned HTML instead of JSON. Check the WFS URL and layer name: {layer_name}" + ) + + return gpd.read_file(response.text) + + +def generate_gpkg(state, district, block, workspace): + """ + Generate GPKG for each location. + """ + + try: + layer_name, gpkg_path, district, block = build_layer_and_path( + state, district, block, workspace + ) + gdf = read_layer_from_geoserver(layer_name) + + if gdf.empty: + print("Layer is empty") + return None + + # CRS handling + if gdf.crs is None: + gdf = gdf.set_crs("EPSG:4326") + else: + gdf = gdf.to_crs("EPSG:4326") + + # Remove existing gpkg + if gpkg_path.exists(): + gpkg_path.unlink() + + # Write gpkg + gdf.to_file( + gpkg_path, + layer=f"{district}_{block}", + driver="GPKG", + ) + print(f"GPKG file created successfully : {gpkg_path}") + return str(gpkg_path) + + except Exception as e: + print(f"FAILED : {e}") + return None diff --git a/utilities/geoserver_utils.py b/utilities/geoserver_utils.py index c55289a1..78b4599a 100644 --- a/utilities/geoserver_utils.py +++ b/utilities/geoserver_utils.py @@ -1,8 +1,11 @@ # inbuilt libraries import io +import logging import os from typing import List, Optional, Set +logger = logging.getLogger(__name__) + # third-party libraries import requests from xmltodict import parse, unparse @@ -246,6 +249,22 @@ def create_workspace(self, workspace: str): else: raise GeoserverException(r.status_code, r.content) + def ensure_workspace(self, workspace: str): + """ + Creates the workspace if it does not already exist. + Never raises — a failed workspace creation is non-fatal; the upload + attempt that follows will surface the real error if the workspace is + truly missing. + """ + try: + url = "{}/rest/workspaces/{}.json".format(self.service_url, workspace) + r = self._requests("get", url) + if r.status_code == 200: + return + self.create_workspace(workspace) + except Exception as e: + logger.warning("ensure_workspace(%s) failed (proceeding anyway): %s", workspace, e) + def delete_workspace(self, workspace: str): """ @@ -320,7 +339,7 @@ def delete_vector_store(self, workspace, store): - store: Name of the vector datastore to delete """ print("inside delete_vector_store") - url = f"{GEOSERVER_URL}/rest/workspaces/{workspace}/datastores/{store}?recurse=true" + url = f"{self.service_url}/rest/workspaces/{workspace}/datastores/{store}?recurse=true" resp = self._requests(method="delete", url=url) if resp.status_code in [200, 202]: print(f"Vector store '{store}' deleted successfully.") @@ -416,14 +435,13 @@ def create_coveragestore( headers = {"content-type": content_type, "Accept": "application/json"} - r = None with open(path, "rb") as f: r = self._requests(method="put", url=url, data=f, headers=headers) - if r.status_code == 201: - return r.json() - else: - raise GeoserverException(r.status_code, r.content) + if r.status_code in (200, 201): + return r.json() + else: + raise GeoserverException(r.status_code, r.content) def upload_raster( self, @@ -513,6 +531,81 @@ def publish_time_dimension_to_coveragestore( else: raise GeoserverException(r.status_code, r.content) + def configure_coverage( + self, + workspace: str, + store: str, + coverage: Optional[str] = None, + srs: str = "EPSG:4326", + projection_policy: str = "FORCE_DECLARED", + enabled: bool = True, + ): + """ + Force a coverage's declared SRS, projection policy, and enabled state. + + Use this right after ``create_coveragestore`` when the source raster + may have missing/unresolvable CRS metadata (e.g. broken PROJ on the + writer side). ``FORCE_DECLARED`` tells GeoServer to treat the declared + SRS as authoritative instead of relying on the file's native CRS. + """ + coverage_name = coverage or store + url = "{0}/rest/workspaces/{1}/coveragestores/{2}/coverages/{3}".format( + self.service_url, workspace, store, coverage_name + ) + body = ( + "" + f"{coverage_name}" + f"{coverage_name}" + f"{str(enabled).lower()}" + f"{srs}" + f"{projection_policy}" + "" + ) + r = self._requests( + method="put", + url=url, + data=body, + headers={"content-type": "application/xml"}, + ) + if r.status_code in (200, 201): + return r.status_code + raise GeoserverException(r.status_code, r.content) + + def configure_featuretype( + self, + workspace: str, + store: str, + featuretype: Optional[str] = None, + srs: str = "EPSG:4326", + projection_policy: str = "FORCE_DECLARED", + ): + """ + Force a featuretype's declared SRS and projection policy. + + Use after publishing a vector store when the source file may have + missing or mismatched CRS metadata. ``FORCE_DECLARED`` treats the + declared SRS as authoritative without reprojecting coordinates. + """ + ft_name = featuretype or store + url = "{0}/rest/workspaces/{1}/datastores/{2}/featuretypes/{3}".format( + self.service_url, workspace, store, ft_name + ) + body = ( + "" + f"{srs}" + f"{projection_policy}" + "" + ) + r = self._requests( + method="put", + url=url, + data=body, + headers={"content-type": "application/xml"}, + ) + if r.status_code in (200, 201): + return r.status_code + raise GeoserverException(r.status_code, r.content) + # delete coveragestore(raster) def delete_raster_store(self, workspace, store): """ @@ -523,7 +616,7 @@ def delete_raster_store(self, workspace, store): - store: Name of the vector datastore to delete """ print("inside delete_raster_store") - url = f"{GEOSERVER_URL}/rest/workspaces/{workspace}/coveragestores/{store}?recurse=true" + url = f"{self.service_url}/rest/workspaces/{workspace}/coveragestores/{store}?recurse=true" resp = self._requests(method="delete", url=url) if resp.status_code in [200, 202]: print(f"Raster store '{store}' deleted successfully.") diff --git a/utilities/layer_generation_logging.py b/utilities/layer_generation_logging.py new file mode 100644 index 00000000..979b19bf --- /dev/null +++ b/utilities/layer_generation_logging.py @@ -0,0 +1,153 @@ +""" +Structured logging for layer-generation HTTP APIs and Celery tasks. +""" + +import logging +import traceback +from functools import wraps + +logger = logging.getLogger("core_stack.layer_generation") + +LAYER_REQUEST_FIELDS = ( + "state", + "district", + "block", + "gee_account_id", + "start_date", + "end_date", + "start_year", + "end_year", + "workspace", + "layer_name", + "layer_id", + "asset_id", +) + + +def extract_request_context(request): + """Best-effort extraction of common layer-generation params from a DRF request.""" + context = {} + if request is None: + return context + try: + body = getattr(request, "data", None) or {} + for key in LAYER_REQUEST_FIELDS: + if key in body: + context[key] = body.get(key) + query_params = getattr(request, "query_params", None) + if query_params: + for key in LAYER_REQUEST_FIELDS: + if key in query_params: + context[key] = query_params.get(key) + except Exception: + pass + return context + + +def log_layer_api_start(view_name, request): + logger.info( + "Layer API start | api=%s context=%s", + view_name, + extract_request_context(request), + ) + + +def log_layer_api_failure(view_name, exc, request=None, extra=None): + logger.error( + "Layer API failed | api=%s context=%s extra=%s error=%s", + view_name, + extract_request_context(request), + extra or {}, + exc, + exc_info=exc, + ) + + +def format_api_error_payload(view_name, exc): + """Build a JSON-serializable error body for API clients and operators.""" + payload = { + "error": str(exc), + "api": view_name, + "exception_type": type(exc).__name__, + } + cause = getattr(exc, "__cause__", None) or getattr(exc, "__context__", None) + if cause: + payload["caused_by"] = f"{type(cause).__name__}: {cause}" + tb = traceback.format_exc() + if tb and tb.strip() not in ("NoneType: None", "NoneType: None\n"): + lines = [line for line in tb.strip().splitlines() if line.strip()] + if lines: + payload["traceback_tail"] = "\n".join(lines[-8:]) + return payload + + +def layer_api_error_response(view_name, exc, request=None): + """Log failure and return a DRF Response with troubleshooting fields.""" + from rest_framework import status + from rest_framework.response import Response + + log_layer_api_failure(view_name, exc, request=request) + return Response( + format_api_error_payload(view_name, exc), + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + +def layer_generation_api_logging(view_func): + """ + Log API entry, unhandled exceptions (with traceback), and 5xx responses + returned from inner try/except blocks. + """ + + @wraps(view_func) + def wrapper(request, *args, **kwargs): + view_name = view_func.__name__ + log_layer_api_start(view_name, request) + try: + response = view_func(request, *args, **kwargs) + except Exception as exc: + return layer_api_error_response(view_name, exc, request=request) + + status_code = getattr(response, "status_code", None) + if status_code is not None and status_code >= 500: + logger.error( + "Layer API returned %s | api=%s context=%s body=%s", + status_code, + view_name, + extract_request_context(request), + getattr(response, "data", None), + ) + return response + + return wrapper + + +def log_task_step(task_name, step, **context): + logger.info( + "Layer task step | task=%s step=%s context=%s", + task_name, + step, + context, + ) + + +def log_task_failure(task_name, exc, **context): + logger.error( + "Layer task failed | task=%s context=%s error=%s", + task_name, + context, + exc, + exc_info=exc, + ) + + +def task_location_context(state=None, district=None, block=None, **extra): + ctx = {} + if state is not None: + ctx["state"] = state + if district is not None: + ctx["district"] = district + if block is not None: + ctx["block"] = block + ctx.update(extra) + return ctx diff --git a/utilities/layer_generation_mode.py b/utilities/layer_generation_mode.py new file mode 100644 index 00000000..4588592c --- /dev/null +++ b/utilities/layer_generation_mode.py @@ -0,0 +1,552 @@ +from functools import wraps +from unittest.mock import patch +import logging +from contextvars import ContextVar + +from celery.app.task import Task +from django.conf import settings + +from utilities.layer_generation_logging import log_task_failure, log_task_step +from utilities.stac_spec_collector import collect_generated_stac_specs + +logger = logging.getLogger("core_stack.layer_generation") +_SYNC_LAYER_GENERATION_CONTEXT = ContextVar( + "sync_layer_generation_context", + default=False, +) +_SYNC_STAC_LAYERS = ContextVar("sync_stac_layers", default=None) +_SYNC_LAYER_IDS = ContextVar("sync_layer_ids", default=None) +_SYNC_LAYER_GENERATED = ContextVar("sync_layer_generated", default=None) +_SYNC_STAC_ERRORS = ContextVar("sync_stac_errors", default=None) + + +def _sync_layer_generation_enabled(): + return bool(getattr(settings, "LAYER_GENERATION_SYNC_MODE", False)) + + +def _get_request_value(data, *keys): + """Read first non-empty value from request body (supports aliases and form lists).""" + if data is None: + return None + for key in keys: + val = data.get(key) + if val is None: + continue + if isinstance(val, (list, tuple)): + val = val[0] if val else None + if val is not None and str(val).strip() != "": + return str(val).strip() + return None + + +def format_stac_for_api_response(stac_payload): + """Return STAC Feature item(s) for the API ``stac`` field, or ``{}`` when absent.""" + if not stac_payload: + return {} + if isinstance(stac_payload, dict) and stac_payload.get("type") == "Feature": + return stac_payload + if isinstance(stac_payload, list): + return stac_payload + if not isinstance(stac_payload, dict): + return {} + items = stac_payload.get("items") or [] + if len(items) == 1: + return items[0] + if len(items) > 1: + return items + return {} + + +def read_location_from_request(request): + """Resolve state/district/block from common request-body key aliases.""" + if request is None or not hasattr(request, "data"): + return None, None, None + data = request.data + state = _get_request_value(data, "state", "State", "STATE") + district = _get_request_value(data, "district", "District", "DISTRICT") + block = _get_request_value( + data, "block", "Block", "BLOCK", "tehsil", "Tehsil", "TEHSIL" + ) + return state, district, block + + +def record_sync_stac_error(message): + errors = _SYNC_STAC_ERRORS.get() + if errors is not None: + errors.append(str(message)) + + +def record_sync_layer_id(layer_id): + """Track a Layer row synced to GeoServer during the current sync request.""" + layer_ids = _SYNC_LAYER_IDS.get() + if layer_ids is None or not layer_id: + return + layer_ids.append(layer_id) + _SYNC_LAYER_GENERATED.set(True) + + +def record_sync_no_layer_generated(): + """Mark that the current sync request completed without creating a layer.""" + if _SYNC_LAYER_GENERATION_CONTEXT.get(): + _SYNC_LAYER_GENERATED.set(False) + + +def record_sync_stac_layer( + *, + state, + district, + block, + layer_name, + layer_type, + start_year="", + end_year="", +): + """Track a layer whose STAC was generated during the current sync request.""" + layers = _SYNC_STAC_LAYERS.get() + if layers is None: + return + layers.append( + { + "state": state, + "district": district, + "block": block, + "layer_name": layer_name, + "layer_type": layer_type, + "start_year": start_year, + "end_year": end_year, + } + ) + + +def _request_layer_targets(request): + """Build layer targets from explicit request fields (e.g. generate_stac_collection).""" + if request is None or not hasattr(request, "data"): + return [] + data = request.data + state = data.get("state") + district = data.get("district") + block = data.get("block") + layer_name = data.get("layer_name") + layer_type = data.get("layer_type") + if not all([state, district, block, layer_name, layer_type]): + return _lulc_stac_targets_from_request(request) + return [ + { + "state": state, + "district": district, + "block": block, + "layer_name": layer_name, + "layer_type": layer_type, + "start_year": data.get("start_year", "") or "", + "end_year": data.get("end_year", "") or "", + } + ] + + +def _lulc_stac_targets_from_request(request): + """STAC target for year-range LULC v3 clip APIs (land_use_land_cover_raster).""" + state, district, block = read_location_from_request(request) + if request is None or not hasattr(request, "data"): + return [] + data = request.data + start_year = data.get("start_year") + end_year = data.get("end_year") + if not all([state, district, block, start_year, end_year]): + return [] + return [ + { + "state": state, + "district": district, + "block": block, + "layer_name": "land_use_land_cover_raster", + "layer_type": "raster", + "start_year": str(start_year), + "end_year": str(end_year), + } + ] + + +def _merge_stac_targets(*target_lists): + merged = [] + seen = set() + for targets in target_lists: + for target in targets or []: + key = ( + target.get("state"), + target.get("district"), + target.get("block"), + target.get("layer_name"), + target.get("layer_type"), + target.get("start_year"), + target.get("end_year"), + ) + if key in seen: + continue + seen.add(key) + merged.append(target) + return merged + + +def _stac_targets_from_layer_ids(layer_ids): + from computing.models import Layer + from computing.stac_layer_resolution import stac_collect_target_for_layer + + targets = [] + seen = set() + for layer_id in layer_ids: + layer = Layer.objects.filter(id=layer_id).first() + if layer is None: + continue + target = stac_collect_target_for_layer(layer) + if target is None: + continue + key = ( + target["state"], + target["district"], + target["block"], + target["layer_name"], + target["layer_type"], + target["start_year"], + target["end_year"], + ) + if key in seen: + continue + seen.add(key) + targets.append(target) + return targets + + +def _ensure_stac_for_sync_layer_ids(layer_ids): + from computing.models import Layer + from computing.STAC_specs.stac_collection import generate_stac_collection_task + from computing.stac_layer_resolution import stac_task_kwargs_for_layer + + for layer_id in dict.fromkeys(layer_ids): + layer = Layer.objects.filter(id=layer_id).first() + if layer is None or layer.is_stac_specs_generated: + continue + task_kwargs = stac_task_kwargs_for_layer(layer) + if task_kwargs is None: + msg = ( + f"No STAC mapping for layer id={layer_id} " + f"(dataset={getattr(layer.dataset, 'name', None)}, " + f"layer_name={layer.layer_name}). " + "Run: python manage.py load_layer_mappings" + ) + logger.warning("Sync STAC skipped: %s", msg) + record_sync_stac_error(msg) + continue + result = generate_stac_collection_task.apply(kwargs=task_kwargs) + if getattr(result, "failed", lambda: False)(): + msg = f"STAC generation failed for layer id={layer_id}: {result.result}" + logger.error(msg) + record_sync_stac_error(msg) + continue + if not result.result: + msg = ( + f"STAC generation returned False for layer id={layer_id} " + f"({task_kwargs['layer_type']}/{task_kwargs['layer_name']}). " + "Check GeoServer layer exists and GEOSERVER_URL in .env." + ) + logger.error(msg) + record_sync_stac_error(msg) + continue + record_sync_stac_layer( + state=task_kwargs["state"], + district=task_kwargs["district"], + block=task_kwargs["block"], + layer_name=task_kwargs["layer_name"], + layer_type=task_kwargs["layer_type"], + start_year=task_kwargs["start_year"], + end_year=task_kwargs["end_year"], + ) + + +def _ensure_stac_for_layer_targets(layer_targets): + from computing.STAC_specs.stac_collection import generate_stac_collection_task + + for target in layer_targets: + existing = collect_generated_stac_specs(**target) + if existing.get("items"): + record_sync_stac_layer( + state=target["state"], + district=target["district"], + block=target["block"], + layer_name=target["layer_name"], + layer_type=target["layer_type"], + start_year=target.get("start_year", "") or "", + end_year=target.get("end_year", "") or "", + ) + continue + + task_kwargs = { + "layer_type": target["layer_type"], + "state": target["state"], + "district": target["district"], + "block": target["block"], + "layer_name": target["layer_name"], + "start_year": target.get("start_year", "") or "", + "end_year": target.get("end_year", "") or "", + "upload_to_s3": bool(getattr(settings, "STAC_UPLOAD_TO_S3", False)), + "overwrite_metadata": bool( + getattr(settings, "STAC_OVERWRITE_METADATA", True) + ), + } + result = generate_stac_collection_task.apply(kwargs=task_kwargs) + if getattr(result, "failed", lambda: False)(): + msg = ( + f"STAC generation failed for {target['layer_type']}/" + f"{target['layer_name']}: {result.result}" + ) + logger.error(msg) + record_sync_stac_error(msg) + continue + if not result.result: + msg = ( + f"STAC generation returned False for {target['layer_type']}/" + f"{target['layer_name']}. Check GeoServer layer exists and " + "GEOSERVER_URL in .env." + ) + logger.error(msg) + record_sync_stac_error(msg) + continue + record_sync_stac_layer( + state=target["state"], + district=target["district"], + block=target["block"], + layer_name=target["layer_name"], + layer_type=target["layer_type"], + start_year=target.get("start_year", "") or "", + end_year=target.get("end_year", "") or "", + ) + + +def _empty_stac_spec(state, district, block): + spec = collect_generated_stac_specs( + state=state, + district=district, + block=block, + layer_name="__none__", + layer_type="vector", + ) + spec["items"] = [] + spec["stac_status"] = "not_generated_yet" + return spec + + +def _collect_stac_for_request(request): + layer_targets = list(_SYNC_STAC_LAYERS.get() or []) + layer_ids = list(_SYNC_LAYER_IDS.get() or []) + + if not layer_targets and layer_ids: + _ensure_stac_for_sync_layer_ids(layer_ids) + layer_targets = _stac_targets_from_layer_ids(layer_ids) + + layer_targets = _merge_stac_targets( + layer_targets, + _request_layer_targets(request), + _lulc_stac_targets_from_request(request), + ) + + if layer_targets: + _ensure_stac_for_layer_targets(layer_targets) + + if not layer_targets: + state, district, block = read_location_from_request(request) + if all([state, district, block]): + if layer_ids: + record_sync_stac_error( + "Layer synced but STAC mapping was not resolved " + "(run python manage.py load_layer_mappings)." + ) + else: + record_sync_stac_error( + "Layer was not marked synced to GeoServer (GeoServer publish may have failed)." + ) + spec = _empty_stac_spec(state, district, block) + errors = list(_SYNC_STAC_ERRORS.get() or []) + if errors: + spec["stac_errors"] = errors + return spec + return None + + merged_items = [] + merged_spec = None + for target in layer_targets: + spec = collect_generated_stac_specs(**target) + if merged_spec is None: + merged_spec = spec + merged_items.extend(spec.get("items") or []) + + if merged_spec is None: + return None + + has_stac_data = bool(merged_items) + merged_spec["items"] = merged_items + merged_spec["stac_status"] = "available" if has_stac_data else "not_generated_yet" + if not has_stac_data and layer_targets: + record_sync_stac_error( + "STAC items not found on disk after generation. " + "Verify GEOSERVER_URL points at the GeoServer where the layer was published." + ) + errors = list(_SYNC_STAC_ERRORS.get() or []) + if errors: + merged_spec["stac_errors"] = errors + if layer_targets: + merged_spec["stac_layers"] = layer_targets + return merged_spec + + +def _read_request_mode(request): + if request is None or not hasattr(request, "data"): + return None + data = request.data + for key in ("layer_generation_mode", "layerGenerationMode", "layer_mode", "mode"): + value = data.get(key) + if value is None: + continue + if isinstance(value, (list, tuple)): + value = value[0] if value else None + if value is None: + continue + normalized = str(value).strip().lower() + if normalized: + return normalized + return None + + +def is_sync_layer_generation_request(request=None): + request_mode = _read_request_mode(request) + if request_mode is not None: + return request_mode == "sync" + return _sync_layer_generation_enabled() + + +def is_sync_layer_generation_context_active(): + return bool(_SYNC_LAYER_GENERATION_CONTEXT.get()) + + +def _apply_async_in_process( + task_self, + args=None, + kwargs=None, + task_id=None, + producer=None, + link=None, + link_error=None, + shadow=None, + **options, +): + """ + Drop-in replacement for Task.apply_async that executes the task immediately. + """ + task_args = args or () + task_kwargs = kwargs or {} + task_name = getattr(task_self, "name", "unknown_task") + log_task_step( + task_name, + "sync_execute_start", + args=task_args, + kwargs=task_kwargs, + ) + + eager_result = task_self.apply( + args=task_args, + kwargs=task_kwargs, + task_id=task_id, + link=link, + link_error=link_error, + **options, + ) + + if hasattr(eager_result, "failed") and eager_result.failed(): + err = getattr(eager_result, "result", "Unknown task failure") + tb = getattr(eager_result, "traceback", None) + log_task_failure( + task_name, + err if isinstance(err, BaseException) else RuntimeError(str(err)), + args=task_args, + kwargs=task_kwargs, + sync_mode=True, + ) + if tb: + logger.error( + "Layer task traceback (sync) | task=%s\n%s", + task_name, + tb, + ) + if isinstance(err, BaseException): + raise RuntimeError(f"{task_name} failed: {err}") from err + raise RuntimeError(f"{task_name} failed: {err}") + + if hasattr(eager_result, "result") and eager_result.result is False: + log_task_failure( + task_name, + RuntimeError("task returned False"), + args=task_args, + kwargs=task_kwargs, + sync_mode=True, + ) + raise RuntimeError(f"{task_name} returned False") + + log_task_step(task_name, "sync_execute_complete", result=eager_result.result) + return eager_result + + +def sync_layer_generation_if_enabled(view_func): + """ + Run Celery task dispatches synchronously for this view when the + LAYER_GENERATION_SYNC_MODE setting is enabled. + """ + + @wraps(view_func) + def wrapper(*args, **kwargs): + request = args[0] if args else kwargs.get("request") + if not is_sync_layer_generation_request(request): + logger.debug( + "Layer generation mode=async for view=%s", + getattr(view_func, "__name__", "unknown"), + ) + return view_func(*args, **kwargs) + logger.info( + "Layer generation mode=sync for view=%s", + getattr(view_func, "__name__", "unknown"), + ) + token = _SYNC_LAYER_GENERATION_CONTEXT.set(True) + stac_layers_token = _SYNC_STAC_LAYERS.set([]) + layer_ids_token = _SYNC_LAYER_IDS.set([]) + layer_generated_token = _SYNC_LAYER_GENERATED.set(None) + stac_errors_token = _SYNC_STAC_ERRORS.set([]) + try: + with patch.object(Task, "apply_async", _apply_async_in_process): + response = view_func(*args, **kwargs) + + try: + payload = getattr(response, "data", None) + if isinstance(payload, dict): + if _SYNC_LAYER_GENERATED.get() is False: + payload.pop("asset_id", None) + payload.pop("asset_ids", None) + payload["layer_generated"] = False + if "stac" not in payload: + stac_spec = _collect_stac_for_request(request) + payload["stac"] = format_stac_for_api_response(stac_spec) + if stac_spec is not None and stac_spec.get("stac_errors"): + payload["stac_errors"] = stac_spec["stac_errors"] + payload.pop("stac_spec", None) + if payload.get("status") == "initiated": + payload["status"] = "completed" + payload["Success"] = "Layer generation completed" + payload["message"] = "Layer generation completed" + except Exception: + logger.exception("Failed to enrich sync response with STAC specs") + finally: + _SYNC_STAC_ERRORS.reset(stac_errors_token) + _SYNC_LAYER_GENERATED.reset(layer_generated_token) + _SYNC_LAYER_IDS.reset(layer_ids_token) + _SYNC_STAC_LAYERS.reset(stac_layers_token) + _SYNC_LAYER_GENERATION_CONTEXT.reset(token) + + return response + + return wrapper diff --git a/utilities/pipelines/__init__.py b/utilities/pipelines/__init__.py index a0960d1e..1bc80a6a 100644 --- a/utilities/pipelines/__init__.py +++ b/utilities/pipelines/__init__.py @@ -7,7 +7,6 @@ """ from .admin import AdminScope, CSAdminSource -from .publish import register_layer from .schema import StandardRequest, api_request_payload, load_config __all__ = [ @@ -15,6 +14,5 @@ "CSAdminSource", "StandardRequest", "api_request_payload", - "register_layer", "load_config", ] diff --git a/utilities/pipelines/admin.py b/utilities/pipelines/admin.py index bb51fff6..3001486c 100644 --- a/utilities/pipelines/admin.py +++ b/utilities/pipelines/admin.py @@ -3,6 +3,7 @@ from __future__ import annotations import re +import sqlite3 from dataclasses import dataclass, field from pathlib import Path from typing import Any, Sequence @@ -84,10 +85,113 @@ def is_internal_admin_column(column: Any) -> bool: return str(column) in INTERNAL_ADMIN_COLUMNS +def normalize_scope_name(value: Any) -> str | None: + """Normalize API-safe separators without changing an admin name.""" + + if value is None: + return None + text = re.sub(r"_+", " ", str(value).strip()) + return " ".join(text.split()) or None + + def normalize_key(value: Any) -> str: """Normalize an admin name for consistent lowercase lookup.""" - return " ".join(str(value or "").strip().lower().split()) + return (normalize_scope_name(value) or "").lower() + + +def admin_name_match_keys(value: Any) -> frozenset[str]: + """Return conservative comparison keys for common admin-name spellings.""" + + text = normalize_scope_name(value) or "" + variants = [text] + variants.extend(re.findall(r"\(([^()]*)\)", text)) + variants.append(re.sub(r"\([^()]*\)", " ", text)) + keys: set[str] = set() + for variant in variants: + words = re.sub(r"[^0-9a-z]+", " ", variant.lower()).split() + if words: + keys.add(" ".join(words)) + keys.add("".join(words)) + return frozenset(keys) + + +def _unique_admin_name_match( + candidates: Sequence[str], requested: str, label: str +) -> str: + """Resolve one name without fuzzy spelling or word-order matching.""" + + exact = [ + candidate + for candidate in candidates + if normalize_key(candidate) == normalize_key(requested) + ] + if len(exact) == 1: + return exact[0] + requested_keys = admin_name_match_keys(requested) + matches = [ + candidate + for candidate in candidates + if requested_keys & admin_name_match_keys(candidate) + ] + if len(matches) != 1: + raise ValueError( + f"Could not uniquely resolve {label} {requested!r}." + ) + return matches[0] + + +def resolve_registration_scope(scope: "AdminScope") -> "AdminScope": + """Resolve a tehsil scope to the exact names stored in Django.""" + + if not (scope.state_name and scope.district_name and scope.tehsil_name): + raise ValueError( + "Layer registration requires state, district, and tehsil names." + ) + + from geoadmin.models import DistrictSOI, StateSOI, TehsilSOI + + def unique_match(queryset, field: str, requested: str, label: str): + exact = list(queryset.filter(**{f"{field}__iexact": requested})[:2]) + if len(exact) == 1: + return exact[0] + matches = [ + item + for item in queryset + if admin_name_match_keys(getattr(item, field)) + & admin_name_match_keys(requested) + ] + if len(matches) != 1: + raise ValueError( + f"Could not uniquely resolve {label} {requested!r} for layer registration." + ) + return matches[0] + + state = unique_match( + StateSOI.objects.all(), + "state_name", + scope.state_name, + "state", + ) + district = unique_match( + DistrictSOI.objects.filter(state=state), + "district_name", + scope.district_name, + "district", + ) + tehsil = unique_match( + TehsilSOI.objects.filter(district=district), + "tehsil_name", + scope.tehsil_name, + "tehsil", + ) + return AdminScope( + level=scope.level, + state_name=state.state_name, + district_name=district.district_name, + tehsil_name=tehsil.tehsil_name, + village_ids=scope.village_ids, + ) def format_admin_name(value: Any) -> str | None: @@ -206,9 +310,13 @@ def from_mapping(cls, data: dict[str, Any]) -> "AdminScope": return cls( level=str(data.get("level") or data.get("scope_level") or "tehsil").lower(), - state_name=data.get("state_name") or data.get("state"), - district_name=data.get("district_name") or data.get("district"), - tehsil_name=data.get("tehsil_name") or data.get("block_name") or data.get("block"), + state_name=normalize_scope_name(data.get("state_name") or data.get("state")), + district_name=normalize_scope_name( + data.get("district_name") or data.get("district") + ), + tehsil_name=normalize_scope_name( + data.get("tehsil_name") or data.get("block_name") or data.get("block") + ), village_ids=_as_tuple(data.get("village_ids") or data.get("village_id")), ) @@ -287,6 +395,63 @@ def ensure_indexes(self) -> list[str]: return ensure_indexes(self.path, self.required_indexes) + def _resolve_scope_aliases(self, scope: AdminScope) -> AdminScope: + """Resolve conservative punctuation/spacing aliases in the GPKG.""" + + level = scope.level.lower() + if level == "village": + return scope + with sqlite3.connect(self.path) as connection: + table = quote_identifier(self.table_name) + state_names = [ + row[0] + for row in connection.execute( + f"SELECT DISTINCT {quote_identifier('state_name')} " + f"FROM {table}" + ) + if row[0] + ] + state = _unique_admin_name_match( + state_names, scope.state_name or "", "state" + ) + district = scope.district_name + tehsil = scope.tehsil_name + if level in {"district", "tehsil", "block"}: + district_names = [ + row[0] + for row in connection.execute( + f"SELECT DISTINCT {quote_identifier('district_name')} " + f"FROM {table} WHERE {lower_key_expression('state_name')} = ?", + (normalize_key(state),), + ) + if row[0] + ] + district = _unique_admin_name_match( + district_names, district or "", "district" + ) + if level in {"tehsil", "block"}: + tehsil_names = [ + row[0] + for row in connection.execute( + f"SELECT DISTINCT {quote_identifier('TEHSIL')} " + f"FROM {table} " + f"WHERE {lower_key_expression('state_name')} = ? " + f"AND {lower_key_expression('district_name')} = ?", + (normalize_key(state), normalize_key(district)), + ) + if row[0] + ] + tehsil = _unique_admin_name_match( + tehsil_names, tehsil or "", "tehsil" + ) + return AdminScope( + level=scope.level, + state_name=state, + district_name=district, + tehsil_name=tehsil, + village_ids=scope.village_ids, + ) + def _where_for_scope(self, scope: AdminScope) -> tuple[str, tuple[Any, ...]]: level = scope.level.lower() clauses: list[str] = [] @@ -360,5 +525,34 @@ def read_scope( params=params, ) if rows.empty: - raise ValueError(f"No admin rows found for scope: {scope}") + resolved_scope = self._resolve_scope_aliases(scope) + resolved_where, resolved_params = self._where_for_scope( + resolved_scope + ) + if include_geometry: + rows = read_features( + self.path, + self.table_name, + columns=read_columns, + where=resolved_where, + params=resolved_params, + geometry_column_name=ADMIN_GEOMETRY_COLUMN, + ) + else: + rows = read_table( + self.path, + self.table_name, + columns=read_columns, + where=resolved_where, + params=resolved_params, + ) + if rows.empty: + raise ValueError(f"No admin rows found for scope: {scope}") + return AdminSelection( + resolved_scope, + rows, + created_indexes, + resolved_where, + resolved_params, + ) return AdminSelection(scope, rows, created_indexes, where, params) diff --git a/utilities/pipelines/outputs.py b/utilities/pipelines/outputs.py index 3381d42a..8068521e 100644 --- a/utilities/pipelines/outputs.py +++ b/utilities/pipelines/outputs.py @@ -23,6 +23,14 @@ def slug(value: Any) -> str: return re.sub(r"[^a-z0-9]+", "_", str(value or "").lower()).strip("_") +def layer_slug(value: Any) -> str: + """Return the established Core Stack spelling for a layer-name part.""" + + from utilities.gee_utils import valid_gee_text + + return valid_gee_text(str(value or "").lower()) + + def utc_now_text() -> str: """Return an ISO UTC timestamp without microseconds.""" @@ -43,18 +51,55 @@ def scope_output_identity(prefix: str, scope: Any) -> tuple[tuple[str, ...], str state = slug(getattr(scope, "state_name", None)) district = slug(getattr(scope, "district_name", None)) tehsil = slug(getattr(scope, "tehsil_name", None)) + layer_state = layer_slug(getattr(scope, "state_name", None)) + layer_district = layer_slug(getattr(scope, "district_name", None)) + layer_tehsil = layer_slug(getattr(scope, "tehsil_name", None)) + layer_prefix = layer_slug(prefix) if level == "state": parts = tuple(part for part in (state,) if part) - return parts, "_".join(part for part in (prefix, state) if part) + return parts, "_".join( + part for part in (layer_prefix, layer_state) if part + ) if level == "district": parts = tuple(part for part in (state, district) if part) - return parts, "_".join(part for part in (prefix, district) if part) + return parts, "_".join( + part for part in (layer_prefix, layer_district) if part + ) if level == "village": village_ids = tuple(str(value) for value in (getattr(scope, "village_ids", None) or ())) digest = stable_hash({"village_ids": village_ids})[:10] if village_ids else "unknown" - return ("village", digest), f"{prefix}_village_{digest}" + return ("village", digest), f"{layer_prefix}_village_{digest}" parts = tuple(part for part in (state, district, tehsil) if part) - return parts, "_".join(part for part in (prefix, district, tehsil) if part) + return parts, "_".join( + part + for part in (layer_prefix, layer_district, layer_tehsil) + if part + ) + + +def resolved_scope_output_identity( + admin_source: Any, + prefix: str, + scope: Any, + *, + include_geometry: bool, +) -> tuple[Any, Any, tuple[str, ...], str]: + """Read a scope and derive its output and DB-compatible layer identity.""" + + from .admin import resolve_registration_scope + + selection = admin_source.read_scope( + scope, + include_geometry=include_geometry, + ) + output_parts, _ = scope_output_identity(prefix, selection.scope) + naming_scope = ( + resolve_registration_scope(scope) + if str(getattr(scope, "level", "") or "").lower() == "tehsil" + else selection.scope + ) + _, layer_name = scope_output_identity(prefix, naming_scope) + return selection, naming_scope, output_parts, layer_name def mark_cached_result(result: Mapping[str, Any], started: float) -> dict[str, Any]: diff --git a/utilities/pipelines/publish.py b/utilities/pipelines/publish.py index ea8f6a6b..f697a5af 100644 --- a/utilities/pipelines/publish.py +++ b/utilities/pipelines/publish.py @@ -405,64 +405,3 @@ def publish_gpkg_layers( property_count=len(verification["properties"]), ) return results - - -def register_layer( - *, - dataset_name: str, - layer_name: str, - scope: Any, - workspace: str, - geoserver_url: str, - algorithm: str, - algorithm_version: str, - misc: dict[str, Any] | None = None, - overwrite: bool = False, -) -> dict[str, Any]: - """Register a published layer in the Core Stack layer database. - - Uses the same `save_layer_info_to_db` path as the GEE-backed pipelines, so - local pipeline layers appear alongside every other layer. The `Layer` model - is keyed on state/district/block, so only tehsil-scoped runs can register; - other scopes are reported as skipped rather than failing the run. - - Never raises: the run has already succeeded by the time this is called, so - a database problem is reported in the result instead of losing the outputs. - """ - - level = str(getattr(scope, "level", "") or "").lower() - if level not in {"tehsil", "block"}: - return {"ok": False, "status": "skipped", "reason": f"layer registration needs a tehsil scope, got {level!r}"} - state, district, block = scope.state_name, scope.district_name, scope.tehsil_name - if not (state and district and block): - return {"ok": False, "status": "skipped", "reason": "scope is missing state, district, or tehsil name"} - - try: - from computing.models import Dataset, LayerType - from computing.utils import save_layer_info_to_db, update_layer_sync_status - - Dataset.objects.get_or_create( - name=dataset_name, - defaults={"layer_type": LayerType.VECTOR, "workspace": workspace}, - ) - layer_id = save_layer_info_to_db( - state=state, - district=district, - block=block, - layer_name=layer_name, - asset_id=geoserver_url, - dataset_name=dataset_name, - algorithm=algorithm, - algorithm_version=algorithm_version, - misc={"is_generated_locally": True, "geoserver_workspace": workspace, **(misc or {})}, - is_override=overwrite, - is_gee_asset=False, - ) - if not layer_id: - return {"ok": False, "status": "not_registered", "dataset": dataset_name, - "reason": "save_layer_info_to_db returned no layer id (check state/district/block exist in the SOI tables)"} - update_layer_sync_status(layer_id=layer_id, sync_to_geoserver=True) - return {"ok": True, "status": "registered", "dataset": dataset_name, "layer_id": layer_id} - except Exception as exc: - return {"ok": False, "status": "registration_failed", "dataset": dataset_name, - "error_type": exc.__class__.__name__, "error": str(exc)[:500]} diff --git a/utilities/pipelines/tabular.py b/utilities/pipelines/tabular.py index 29fb3976..f3aad2c4 100644 --- a/utilities/pipelines/tabular.py +++ b/utilities/pipelines/tabular.py @@ -135,7 +135,12 @@ def fetch_by_values( values = [value for value in values if value is not None and value == value] if not values: - return pd.DataFrame(columns=list(columns or [])) + empty_columns = ( + columns + if columns is not None + else (*self.key_columns, *(self.source_columns or ())) + ) + return pd.DataFrame(columns=list(dict.fromkeys(empty_columns))) if not self.is_fresh(): self.materialize() select_columns = "*" if columns is None else ", ".join(quote_identifier(col) for col in columns) diff --git a/utilities/scripts/hwsd.py b/utilities/scripts/hwsd.py new file mode 100644 index 00000000..376caa9a --- /dev/null +++ b/utilities/scripts/hwsd.py @@ -0,0 +1,99 @@ +import geopandas as gpd +import pandas as pd +import numpy as np +import rasterio +from rasterio.mask import mask + +# ------------------------------------------------------------------ +# INPUTS +# ------------------------------------------------------------------ + +HWSD_RASTER = "data/HWSD_RASTER/hwsd_wgs84.tif" +HWSD_CSV = "data/HWSD_DATA.csv" +INDIA_BOUNDARY = "data/india_state_outer_no_islands.geojson" + +OUTPUT_RASTER = "data/india_subsoil_texture.tif" + +# ------------------------------------------------------------------ +# LOAD INDIA BOUNDARY +# ------------------------------------------------------------------ + +india = gpd.read_file(INDIA_BOUNDARY) + +if india.crs is None: + india = india.set_crs("EPSG:4326") +# ------------------------------------------------------------------ +# LOAD LOOKUP TABLE +# ------------------------------------------------------------------ + +df = pd.read_csv(HWSD_CSV, usecols=["MU_GLOBAL", "S_USDA_TEX_CLASS"], low_memory=False) + +lookup_df = df[["MU_GLOBAL", "S_USDA_TEX_CLASS"]].dropna().drop_duplicates("MU_GLOBAL") + +max_mu = int(lookup_df["MU_GLOBAL"].max()) + +# Lookup table: +# lut[MU_GLOBAL] = S_USDA_TEX_CLASS +lut = np.zeros(max_mu + 1, dtype=np.int16) + +lut[lookup_df["MU_GLOBAL"].astype(int)] = lookup_df["S_USDA_TEX_CLASS"].astype(int) + +# ------------------------------------------------------------------ +# CLIP HWSD TO INDIA +# ------------------------------------------------------------------ + +with rasterio.open(HWSD_RASTER) as src: + print(src.crs) # None + + if src.crs is None: + raster_crs = "EPSG:4326" + else: + raster_crs = src.crs + + if india.crs != raster_crs: + india = india.to_crs(raster_crs) + + clipped, transform = mask(src, india.geometry, crop=True, nodata=0) + + profile = src.profile.copy() + +mu_global = clipped[0] + +# ------------------------------------------------------------------ +# CONVERT MU_GLOBAL -> SUBSOIL TEXTURE CLASS +# ------------------------------------------------------------------ + +texture = np.zeros(mu_global.shape, dtype=np.int16) + +valid = (mu_global > 0) & (mu_global <= max_mu) + +texture[valid] = lut[mu_global[valid]] + +# ------------------------------------------------------------------ +# SAVE OUTPUT +# ------------------------------------------------------------------ + +profile.update( + driver="GTiff", + height=texture.shape[0], + width=texture.shape[1], + transform=transform, + dtype=rasterio.int16, + nodata=0, + compress="lzw", +) + +with rasterio.open(OUTPUT_RASTER, "w", **profile) as dst: + dst.write(texture, 1) + +print(f"Saved: {OUTPUT_RASTER}") + +# ------------------------------------------------------------------ +# OPTIONAL: PRINT CLASS DISTRIBUTION +# ------------------------------------------------------------------ + +unique, counts = np.unique(texture[texture > 0], return_counts=True) + +print("\nTexture classes found in India:") +for cls, cnt in zip(unique, counts): + print(f"Class {cls}: {cnt:,} pixels") diff --git a/utilities/scripts/tree_health/colab_notebooks/Generate_LTP_STP_Polygons.ipynb b/utilities/scripts/tree_health/colab_notebooks/Generate_LTP_STP_Polygons.ipynb new file mode 100644 index 00000000..b08f77ae --- /dev/null +++ b/utilities/scripts/tree_health/colab_notebooks/Generate_LTP_STP_Polygons.ipynb @@ -0,0 +1,3349 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "id": "-KTc04_9KI79", + "outputId": "708f410f-2793-4db6-a61c-9a516f60a9c2" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + " " + ] + }, + "metadata": {} + } + ], + "source": [ + "import geemap, ee\n", + "\n", + "ee.Authenticate()\n", + "ee.Initialize(project=\"corestack-datasets-alpha\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 35 + }, + "id": "sZrP5IEgKWXq", + "outputId": "2265e164-9a63-4df8-f2a4-c185e44b4adb" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Mounted at /content/drive\n" + ] + } + ], + "source": [ + "from google.colab import drive\n", + "drive.mount('/content/drive')" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "id": "Y49bxpDIbQZ7", + "outputId": "eb288a20-b66c-49df-cb55-39a231af8793" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + " " + ] + }, + "metadata": {} + } + ], + "source": [ + "import geemap\n", + "import pandas as pd\n", + "import re" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "id": "wxzXAgvvbpzx", + "outputId": "70556940-30a3-4b08-f396-7109de247858" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + " " + ] + }, + "metadata": {} + } + ], + "source": [ + "agroclimaticZone_acronym_dict = {\n", + " # 'Eastern Plateau & Hills Region': 'EPAHR',\n", + " # 'Southern Plateau and Hills Region': 'SPAHR',\n", + " 'East Coast Plains & Hills Region': 'ECPHR',\n", + " # 'Western Plateau and Hills Region': 'WPAHR',\n", + " # 'Central Plateau & Hills Region': 'CPAHR',\n", + " # 'Lower Gangetic Plain Region': 'LGPR',\n", + " # 'Middle Gangetic Plain Region': 'MGPR',\n", + " # 'Eastern Himalayan Region': 'EHR',\n", + " # 'Western Himalayan Region': 'WHR',\n", + " # 'Upper Gangetic Plain Region': 'UGPR',\n", + " # 'Trans Gangetic Plain Region': 'TGPR',\n", + " # 'West Coast Plains & Ghat Region': 'WCPGR',\n", + " # 'Gujarat Plains & Hills Region': 'GPHR',\n", + " # 'Western Dry Region': 'WDR'\n", + "}\n", + "agro_zones = list(agroclimaticZone_acronym_dict.keys())" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "id": "gMYImDlfLoHR", + "outputId": "62590a91-47d8-4201-a318-aedceb8bb825" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + " " + ] + }, + "metadata": {} + } + ], + "source": [ + "years = [2017, 2023]\n", + "india_districts = ee.FeatureCollection(\"projects/ee-indiasat/assets/india_district_boundaries\")\n", + "india_acz = ee.FeatureCollection(\"projects/ee-mtpictd/assets/harsh/Agroclimatic_regions\")\n", + "# agro_dist_df = pd.read_csv(\"drive/MyDrive/dhruvi/data/distict_agroclimate_map.csv\")\n", + "# dist_list = list(agro_dist_df['Name'])" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "id": "wxOORQU1GIxv", + "outputId": "5b1c19dd-0e80-41c1-b72b-5adb11896c65" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + " " + ] + }, + "metadata": {} + } + ], + "source": [ + "err = ee.ErrorMargin(200)\n", + "\n", + "def addArea(feature):\n", + " # Computes the feature's geometry area and adds it as a property\n", + " patch_area = ee.Number(feature.geometry(err).area(err).divide(1e4)) #Converting area to hectare\n", + " ltp = ee.Algorithms.If(patch_area.gte(ee.Number(1)), 1, 0)\n", + " return feature.set({\"area_ha\": patch_area, \"large_tree_patch\": ltp})\n", + "\n", + "def export_image_asset(img, aoi, img_name, year):\n", + " # projection = ltp_img.select('large_tree_patch').projection().getInfo()\n", + " task = ee.batch.Export.image.toAsset(\n", + " image = img,\n", + " description = img_name,\n", + " assetId = 'projects/corestack-trees/assets/tree_characteristics/final_ltp_stp_'+ year + '/' + img_name,\n", + " region = aoi,\n", + " scale = 25,\n", + " crs = 'EPSG:4326',\n", + " # crs = projection['crs'],\n", + " # crsTransform = projection['transform'],\n", + " maxPixels = 1e13\n", + " )\n", + " task.start()\n", + " return task" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 17 + }, + "id": "AXhs2wCl-q79", + "outputId": "6956cc45-ccf2-4de4-d800-984cfbb537c2" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + " " + ] + }, + "metadata": {} + } + ], + "source": [ + "# tree_cover.projection().getInfo()\n", + "\n", + "def get_is_tree_cover(aoi, curr_year, scale = 25):\n", + " curr_year = int(curr_year)\n", + " indiasat_asset = f\"projects/corestack-datasets/assets/datasets/LULC_v3_river_basin/pan_india_lulc_v3_{curr_year}_{curr_year+1}\"\n", + " lulc_image = ee.Image(indiasat_asset).select(\"predicted_label\").clip(aoi)\n", + " return lulc_image.updateMask(lulc_image.eq(6)).toInt().reproject(crs='EPSG:4326', scale=scale)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "debm1g-saQo1", + "outputId": "f4f3d9a2-b975-4cb2-8dea-3e5f80145676", + "collapsed": true + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "East Coast Plains & Hills Region\n", + "Total Districts: 68\n", + "0 Chittoor\n", + "1 East Godavari\n", + "2 Guntur\n", + "3 Krishna\n", + "4 Kurnool\n", + "5 Nellore\n", + "6 Prakasam\n", + "7 Srikakulam\n", + "8 Visakhapatnam\n", + "9 Vizianagaram\n", + "10 West Godavari\n", + "11 Y.S.R.\n", + "12 Idukki\n", + "13 Kollam\n", + "14 Pathanamthitta\n", + "15 Thiruvananthapuram\n", + "16 Anugul\n", + "17 Baleshwar\n", + "18 Bauda\n", + "19 Bhadrak\n", + "20 Cuttack\n", + "21 Dhenkanal\n", + "22 Gajapati\n", + "23 Ganjam\n", + "24 Jagatsinghapur\n", + "25 Jajapur\n", + "26 Kandhamal\n", + "27 Kendrapara\n", + "28 Kendujhar\n", + "29 Khordha\n", + "30 Koraput\n", + "31 Malkangiri\n", + "32 Mayurbhanj\n", + "33 Nayagarh\n", + "34 Puri\n", + "35 Rayagada\n", + "36 Karaikal\n", + "37 Puducherry\n", + "38 Yanam\n", + "39 Ariyalur\n", + "40 Chennai\n", + "41 Cuddalore\n", + "42 Dharmapuri\n", + "43 Kancheepuram\n", + "44 Kanniyakumari\n", + "45 Krishnagiri\n", + "46 Madurai\n", + "47 Nagappattinam\n", + "48 Pudukkottai\n", + "49 Ramanathapuram\n", + "50 Salem\n", + "51 Sivaganga\n", + "52 Thanjavur\n", + "53 Theni\n", + "54 Thiruvallur\n", + "55 Thiruvarur\n", + "56 Thoothukkudi\n", + "57 Tiruchirappalli\n", + "58 Tirunelveli\n", + "59 Tiruvannamalai\n", + "60 Vellore\n", + "61 Viluppuram\n", + "62 Virudunagar\n", + "63 Khammam\n", + "64 Mahbubnagar\n", + "65 Nalgonda\n", + "66 Pashchim Medinipur\n", + "67 Purba Medinipur\n", + "East Coast Plains & Hills Region\n", + "Total Districts: 68\n", + "0 Chittoor\n", + "1 East Godavari\n", + "2 Guntur\n", + "3 Krishna\n", + "4 Kurnool\n", + "5 Nellore\n", + "6 Prakasam\n", + "7 Srikakulam\n", + "8 Visakhapatnam\n", + "9 Vizianagaram\n", + "10 West Godavari\n", + "11 Y.S.R.\n", + "12 Idukki\n", + "13 Kollam\n", + "14 Pathanamthitta\n", + "15 Thiruvananthapuram\n", + "16 Anugul\n", + "17 Baleshwar\n", + "18 Bauda\n", + "19 Bhadrak\n", + "20 Cuttack\n", + "21 Dhenkanal\n", + "22 Gajapati\n", + "23 Ganjam\n", + "24 Jagatsinghapur\n", + "25 Jajapur\n", + "26 Kandhamal\n", + "27 Kendrapara\n", + "28 Kendujhar\n", + "29 Khordha\n", + "30 Koraput\n", + "31 Malkangiri\n", + "32 Mayurbhanj\n", + "33 Nayagarh\n", + "34 Puri\n", + "35 Rayagada\n", + "36 Karaikal\n", + "37 Puducherry\n", + "38 Yanam\n", + "39 Ariyalur\n", + "40 Chennai\n", + "41 Cuddalore\n", + "42 Dharmapuri\n", + "43 Kancheepuram\n", + "44 Kanniyakumari\n", + "45 Krishnagiri\n", + "46 Madurai\n", + "47 Nagappattinam\n", + "48 Pudukkottai\n", + "49 Ramanathapuram\n", + "50 Salem\n", + "51 Sivaganga\n", + "52 Thanjavur\n", + "53 Theni\n", + "54 Thiruvallur\n", + "55 Thiruvarur\n", + "56 Thoothukkudi\n", + "57 Tiruchirappalli\n", + "58 Tirunelveli\n", + "59 Tiruvannamalai\n", + "60 Vellore\n", + "61 Viluppuram\n", + "62 Virudunagar\n", + "63 Khammam\n", + "64 Mahbubnagar\n", + "65 Nalgonda\n", + "66 Pashchim Medinipur\n", + "67 Purba Medinipur\n" + ] + } + ], + "source": [ + "ltp_task = dict()\n", + "for year in years:\n", + " year_0 = str(year-1)\n", + " year_1 = str(year)\n", + " year_2 = str(year+1)\n", + " # tree_cover_india = ee.ImageCollection(f'projects/ee-mtpictd/assets/harsh/dw_corrected_{year}')\n", + " for acz in agro_zones:\n", + " # for acz, acronym in agroclimaticZone_acronym_dict.items():\n", + " print(acz)\n", + " ltp_task[year] = dict()\n", + " # aoi = india_acz.filter(ee.Filter.eq('regionname', acz)).geometry()\n", + " df = pd.read_csv(f'/content/drive/MyDrive/TreeHealth/Agroclimatic_regions/{acz}.csv')\n", + " dist_list = list(df['Name'])\n", + " print(\"Total Districts:\", len(dist_list))\n", + " for i, district in enumerate(dist_list):\n", + " print(i, district)\n", + " aoi = india_districts.filter(ee.Filter.eq('Name', district)).geometry()\n", + "\n", + " # Load DW tree cover corrected data for year_0\n", + " # tree_cover = ee.ImageCollection(f'projects/ee-mtpictd/assets/harsh/dw_corrected_{year}') \\\n", + " # .select(f'label_{year_0[-2:]}') \\\n", + " ######################################\n", + " # ccd = ee.ImageCollection(f'projects/corestack-trees/assets/tree_characteristics/modal_ccd_{year_1}') \\\n", + " # .filterBounds(aoi) \\\n", + " # .mode() \\\n", + " # .clip(aoi)\n", + " # tree_cover = ccd.unmask(-9999)\n", + " # tree_cover = ccd.expression(\n", + " # \"((b('cc')!=-9999)) ? 1 : (-9999)\"\n", + " # ).clip(aoi);\n", + " tree_cover = get_is_tree_cover(aoi, year) # Added this\n", + " tree_cover = tree_cover.rename(['label']);\n", + " tree_cover = tree_cover.updateMask(tree_cover.neq(-9999))\n", + " #########################################\n", + "\n", + " # tree_cover_0 = ee.ImageCollection(f'projects/ee-mtpictd/assets/harsh/tree_cover_{year_0}') \\\n", + " # .filterBounds(aoi) \\\n", + " # .mode() \\\n", + " # .clip(aoi)\n", + " # tree_cover_0 = tree_cover_0.rename(['label_0'])\n", + " # # tree_cover = tree_cover.updateMask(tree_cover.eq(1))\n", + " # # tree_cover = tree_cover.reproject(crs='EPSG:4326', scale=25)\n", + " # # print(\"Tree Cover:\", tree_cover.getInfo())\n", + "\n", + " # # Load DW tree cover corrected data for year_1\n", + " # tree_cover_1 = ee.ImageCollection(f'projects/ee-mtpictd/assets/harsh/tree_cover_{year_1}') \\\n", + " # .filterBounds(aoi) \\\n", + " # .mode() \\\n", + " # .clip(aoi)\n", + " # tree_cover_1 = tree_cover_1.rename(['label_1'])\n", + "\n", + " # # Load DW tree cover corrected data for year_2\n", + " # tree_cover_2 = ee.ImageCollection(f'projects/ee-mtpictd/assets/harsh/tree_cover_{year_2}') \\\n", + " # .filterBounds(aoi) \\\n", + " # .mode() \\\n", + " # .clip(aoi)\n", + " # tree_cover_2 = tree_cover_2.rename(['label_2'])\n", + "\n", + " # # Take modal of all the 3-years tree cover images\n", + " # tree_cover = tree_cover_0.addBands(tree_cover_1).addBands(tree_cover_2);\n", + " # tree_cover = tree_cover.unmask(-9999)\n", + " # tree_cover = tree_cover.expression(\n", + " # \"((b('label_1')!=-9999)) ? (b('label_1'))\"+\n", + " # \":((b('label_0')!=-9999) and (b('label_1')==-9999) and (b('label_2')!=-9999)) ? (b('label_0'))\"+\n", + " # \":(-9999)\"\n", + " # ).clip(aoi);\n", + " # tree_cover = tree_cover.rename(['label']);\n", + " # tree_cover = tree_cover.updateMask(tree_cover.neq(-9999))\n", + "\n", + "\n", + " # tree_patches = tree_cover.reduceToVectors(geometry=aoi, maxPixels=1e13)\n", + " tree_patches = tree_cover.reduceToVectors(crs='EPSG:4326', scale=25, geometry=aoi, maxPixels=1e13, geometryType='polygon', bestEffort=True)\n", + " # print(\"Tree Patches:\", tree_patches.first().getInfo())\n", + "\n", + " tree_patches = tree_patches.map(addArea)\n", + " # print(\"Consecutive tree patches: \", tree_patches.first().getInfo());\n", + " # print(\"Total tree patches: \", tree_patches.size());\n", + "\n", + " ltp_img = ee.Image(0)\n", + " ltp_class = tree_patches.reduceToImage(properties = ['large_tree_patch'], reducer = ee.Reducer.mean()).rename(\"large_tree_patch\")\n", + " ltp_img = ltp_img.addBands(ltp_class).select(['large_tree_patch']).clip(aoi)\n", + " ltp_img = ltp_img.updateMask(tree_cover.select('label'))\n", + " ltp_img = ltp_img.reproject('EPSG:4326', None, 25).clip(aoi).select('large_tree_patch')\n", + "\n", + " # tp_area = tree_patches.reduceToImage(properties = ['area_ha'], reducer = ee.Reducer.first()).rename(\"area_ha\")\n", + " # ltp_img = ltp_img.addBands(ltp_class).addBands(tp_area).select(['large_tree_patch', 'area_ha']).clip(aoi)\n", + " ltp_img = ltp_img.addBands(ltp_class).select(['large_tree_patch']).clip(aoi)\n", + " # ltp_img = ltp_img.updateMask(tree_cover.select(f'label_{year[-2:]}'))\n", + " ltp_img = ltp_img.updateMask(tree_cover.select('label'))\n", + " ltp_img = ltp_img.reproject('EPSG:4326', None, 25).clip(aoi).select('large_tree_patch')\n", + " # ltp_img = ltp_img.reproject('EPSG:4326', tree_cover.projection().getInfo()['transform'], None).clip(aoi).select('large_tree_patch')\n", + " # print(\"LTP Image:\", ltp_img.getInfo());\n", + "\n", + " # var large_tree_patches = tree_patches.filter(ee.Filter.eq('large_tree_patch', 1));\n", + " # print(\"Consecutive large tree patches: \", large_tree_patches.first());\n", + " # # print(\"Number of large tree patches: \", large_tree_patches.size());\n", + "\n", + " # // var empty = ee.Image().byte();\n", + " # // var ltp_boundary = empty.paint(large_tree_patches).paint(large_tree_patches, 0, 1);\n", + " # Map.addLayer(large_tree_patches, {color: 'blue'}, 'LTP');\n", + "\n", + " # var small_tree_patches = tree_patches.filter(ee.Filter.eq('large_tree_patch', 0));\n", + " # // var small_tree_patches = tree_patches.filter(ee.Filter.lt('areaHa', 1));\n", + " # print(\"Consecutive small tree patches: \", small_tree_patches.first());\n", + "\n", + " img_name = 'ltp_' + year_1 + '_' + agroclimaticZone_acronym_dict[acz] + '_' + re.sub(\"[^A-Za-z0-9]\", \"\", district)\n", + " ltp_task[year][district] = export_image_asset(ltp_img, aoi, img_name, year_1)\n", + " # img_name = 'ltp_' + year + '_' + acronym\n", + " # ltp_task[year][acronym] = export_image_asset(ltp_img, aoi, img_name, year)\n", + " # print(\"Done!\")\n", + " # break\n", + " # break\n", + "\n", + " # break" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pN6jV4oNowEl" + }, + "outputs": [], + "source": [ + "# ltp_task['2021']['Shamli'].status()" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 621, + "referenced_widgets": [ + "857ed1d74c924dc38b4216a6b3b3a407", + "efcbaec5b39242ccbf43b53307a91db3", + "2fa90b5240214e6db1a234a6ac9f9d26", + "a3384db27f0e4512b8f801cb446550b8", + "8430b385dd0943cc8515e46374fb5353", + "1a841aa5972647d087d24fd036f4768a", + "c63675a6e3264b4aa1177102b7ee30d7", + "bfaf542bd2514506aa5f675efa7cb807", + "816db69faa5f403782a95534ae89e5b0", + "45e9f57280494b81bedec5a978abd60d", + "3ce4cd936a65472285609d916a781836", + "2ede49326b5c40228d616aab967910e0", + "ceae30f50513407ab97fb3321255b65e", + "a3a057d1ddb0491bbbac924c775080ea", + "23acd2e7dfea4fb7891dd6241fe91626", + "f70085e859494024a296847249f0019d", + "4f56216671684026b4d7dc7131aaeced", + "6ac4b53d3f9a477a8a50b91f3c1a78a1", + "6d191e6e3f2944c89e802cfaf2f4d973", + "11573ba3646148968e821ccd3cc82ee3", + "08df80e9b6e94b60a1f7b30a1e96b6b8", + "0164447f09534c85a9bc9ba3b0a8ced5", + "a0a9213c4e3e464287da4bb6517408e5", + "50efdb138316463cbdb7fcafc36e4f0d", + "d6061d8c11874c50a4379be53a5e3029", + "0f756d3b935c4b14a4aca5d85723c84d", + "d25e876b06b84a57a476a14ce924b60a", + "320be3525f7244e59fa1e32a4cd9bcf3", + "018bc35395594b90912c927e2211212e", + "1e753e5cf2ff4b53ac41e818e9f7bf7b", + "d110ce8e35f140d585e81e88cce4a413", + "060518aa3c224037930316d880e07fc8", + "b9aa9ab9f4044505bdf204d794b88df3", + "9b1e99aba6cb4b7782d78e56ea005d9d", + "c6576ff54bff4e7a9fb410443d3ecc5b", + "4e5bda9157e7494bafcaf8e44f864424", + "e6c172ac8c3f4e4ca08400f839230174", + "b97ac9c1ea2e46e4a0bd07ab88ae012c", + "262c8654c40643898621dfb02718aeb4", + "6907b83887184f18a9811508ec9887f9", + "db63bba9f9c64f4787c328766671edcb", + "5c89fe5b921d45bbbe5594552c80ddd6", + "ac2cd52fca484632832a8ffc460f2e98", + "9b1aa26a079f4cdb933d90202e373ea5", + "7dc38477ca5145948632181e660a72ca", + "3117c8f8dc454974ad725820bbaff1b6", + "f306390cbeee4c17acad2a7faea2bda7", + "bd44dc656406487e96cbe194c6261bd0", + "e9f6bd2414c84f258c433913fe8cf03e", + "f0268b4fa5f8426aab5467c9b24a5fdd", + "c5f72af9d368407dbaf5d9e4d7b91392", + "91e3f52fd7364d319305e5af41f1c82c", + "125e8e3b0ac04f6993b13c51a0380138", + "ea343d9e0bb749998ee948e3b48e087f", + "8038eb785016462b8c9b593921119002", + "fbb8c80ca0784cef9d6139a5fbbb6b46", + "49cfc07507794d6aab0419f0f0f17ef4", + "44d6efda35304e3b82becaa0554cc00a", + "a386181ab2a948f39200afa36498a19e", + "313ce92ad01147d1bd2f3e3491f0fabf", + "b6f2cd884b4140c2b954178b4a3f5ffb", + "8dc8b607f2e64e47a962f0a6a9932837", + "e3c9b813e43e46708e9a73470ba31ce6", + "13e1ae2b10a04cf9a8dfb8bcdfda78b9" + ] + }, + "id": "0SOa06BPnvCw", + "outputId": "28a535c0-56e2-40ae-91da-fcc23a0ce6f4" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Map(center=[22.02646193620022, 87.77414646078844], controls=(WidgetControl(options=['position', 'transparent_b…" + ], + "application/vnd.jupyter.widget-view+json": { + "version_major": 2, + "version_minor": 0, + "model_id": "857ed1d74c924dc38b4216a6b3b3a407" + } + }, + "metadata": { + "application/vnd.jupyter.widget-view+json": { + "colab": { + "custom_widget_manager": { + "url": "https://ssl.gstatic.com/colaboratory-static/widgets/colab-cdn-widget-manager/2b70e893a8ba7c0f/manager.min.js" + } + } + } + } + } + ], + "source": [ + "Map = geemap.Map(basemap='HYBRID')\n", + "Map.addLayer(tree_cover, {'palette': 'green'}, '25m DW Tree Cover')\n", + "# Map.addLayer(tree_patches, {'palette': 'yellow'}, 'Tree Patches')\n", + "Map.addLayer(ltp_img, {'bands':['large_tree_patch'], 'min': 0, 'max': 1, 'palette': ['FFA500', '007500']}, 'LTP-STP Image')\n", + "Map.centerObject(ltp_img, 15);\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uGqE3hCKKan0" + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9ANwJOILKWGn" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "857ed1d74c924dc38b4216a6b3b3a407": { + "model_module": "jupyter-leaflet", + "model_name": "LeafletMapModel", + "model_module_version": "^0.20", + "state": { + "_dom_classes": [], + "_model_module": "jupyter-leaflet", + "_model_module_version": "^0.20", + "_model_name": "LeafletMapModel", + "_view_count": null, + "_view_module": "jupyter-leaflet", + "_view_module_version": "^0.20", + "_view_name": "LeafletMapView", + "bottom": 3668220, + "bounce_at_zoom_limits": true, + "box_zoom": true, + "center": [ + 22.02646193620022, + 87.77414646078844 + ], + "close_popup_on_click": true, + "controls": [ + "IPY_MODEL_efcbaec5b39242ccbf43b53307a91db3", + "IPY_MODEL_2fa90b5240214e6db1a234a6ac9f9d26", + "IPY_MODEL_a3384db27f0e4512b8f801cb446550b8", + "IPY_MODEL_8430b385dd0943cc8515e46374fb5353", + "IPY_MODEL_1a841aa5972647d087d24fd036f4768a", + "IPY_MODEL_c63675a6e3264b4aa1177102b7ee30d7", + "IPY_MODEL_bfaf542bd2514506aa5f675efa7cb807", + "IPY_MODEL_816db69faa5f403782a95534ae89e5b0" + ], + "crs": { + "name": "EPSG3857", + "custom": false + }, + "default_style": "IPY_MODEL_45e9f57280494b81bedec5a978abd60d", + "double_click_zoom": true, + "dragging": true, + "dragging_style": "IPY_MODEL_3ce4cd936a65472285609d916a781836", + "east": 87.80651092529298, + "fullscreen": false, + "inertia": true, + "inertia_deceleration": 3000, + "inertia_max_speed": 1500, + "interpolation": "bilinear", + "keyboard": true, + "keyboard_pan_offset": 80, + "keyboard_zoom_offset": 1, + "layers": [ + "IPY_MODEL_2ede49326b5c40228d616aab967910e0", + "IPY_MODEL_ceae30f50513407ab97fb3321255b65e", + "IPY_MODEL_a3a057d1ddb0491bbbac924c775080ea" + ], + "layout": "IPY_MODEL_23acd2e7dfea4fb7891dd6241fe91626", + "left": 6238836, + "max_zoom": 24, + "min_zoom": null, + "modisdate": "2026-07-10", + "north": 22.038389590665307, + "options": [ + "bounce_at_zoom_limits", + "box_zoom", + "center", + "close_popup_on_click", + "double_click_zoom", + "dragging", + "fullscreen", + "inertia", + "inertia_deceleration", + "inertia_max_speed", + "interpolation", + "keyboard", + "keyboard_pan_offset", + "keyboard_zoom_offset", + "max_zoom", + "min_zoom", + "prefer_canvas", + "scroll_wheel_zoom", + "tap", + "tap_tolerance", + "touch_zoom", + "world_copy_jump", + "zoom", + "zoom_animation_threshold", + "zoom_delta", + "zoom_snap" + ], + "panes": {}, + "prefer_canvas": false, + "right": 6240344, + "scroll_wheel_zoom": true, + "south": 22.014519798548438, + "style": "IPY_MODEL_f70085e859494024a296847249f0019d", + "tap": true, + "tap_tolerance": 15, + "top": 3667620, + "touch_zoom": true, + "west": 87.74179458618164, + "window_url": "https://zlji9riebaj-496ff2e9c6d22116-0-colab.googleusercontent.com/outputframe.html?vrz=colab-external_20260707-060055_RC00_943753072", + "world_copy_jump": false, + "zoom": 15, + "zoom_animation_threshold": 4, + "zoom_delta": 1, + "zoom_snap": 1 + } + }, + "efcbaec5b39242ccbf43b53307a91db3": { + "model_module": "jupyter-leaflet", + "model_name": "LeafletWidgetControlModel", + "model_module_version": "^0.20", + "state": { + "_model_module": "jupyter-leaflet", + "_model_module_version": "^0.20", + "_model_name": "LeafletWidgetControlModel", + "_view_count": null, + "_view_module": "jupyter-leaflet", + "_view_module_version": "^0.20", + "_view_name": "LeafletWidgetControlView", + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "options": [ + "position", + "transparent_bg" + ], + "position": "topright", + "transparent_bg": true, + "widget": "IPY_MODEL_4f56216671684026b4d7dc7131aaeced" + } + }, + "2fa90b5240214e6db1a234a6ac9f9d26": { + "model_module": "jupyter-leaflet", + "model_name": "LeafletWidgetControlModel", + "model_module_version": "^0.20", + "state": { + "_model_module": "jupyter-leaflet", + "_model_module_version": "^0.20", + "_model_name": "LeafletWidgetControlModel", + "_view_count": null, + "_view_module": "jupyter-leaflet", + "_view_module_version": "^0.20", + "_view_name": "LeafletWidgetControlView", + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "options": [ + "position", + "transparent_bg" + ], + "position": "topleft", + "transparent_bg": true, + "widget": "IPY_MODEL_6ac4b53d3f9a477a8a50b91f3c1a78a1" + } + }, + "a3384db27f0e4512b8f801cb446550b8": { + "model_module": "jupyter-leaflet", + "model_name": "LeafletZoomControlModel", + "model_module_version": "^0.20", + "state": { + "_model_module": "jupyter-leaflet", + "_model_module_version": "^0.20", + "_model_name": "LeafletZoomControlModel", + "_view_count": null, + "_view_module": "jupyter-leaflet", + "_view_module_version": "^0.20", + "_view_name": "LeafletZoomControlView", + "options": [ + "position", + "zoom_in_text", + "zoom_in_title", + "zoom_out_text", + "zoom_out_title" + ], + "position": "topleft", + "zoom_in_text": "+", + "zoom_in_title": "Zoom in", + "zoom_out_text": "-", + "zoom_out_title": "Zoom out" + } + }, + "8430b385dd0943cc8515e46374fb5353": { + "model_module": "jupyter-leaflet", + "model_name": "LeafletFullScreenControlModel", + "model_module_version": "^0.20", + "state": { + "_model_module": "jupyter-leaflet", + "_model_module_version": "^0.20", + "_model_name": "LeafletFullScreenControlModel", + "_view_count": null, + "_view_module": "jupyter-leaflet", + "_view_module_version": "^0.20", + "_view_name": "LeafletFullScreenControlView", + "options": [ + "position" + ], + "position": "topleft" + } + }, + "1a841aa5972647d087d24fd036f4768a": { + "model_module": "jupyter-leaflet", + "model_name": "LeafletDrawControlModel", + "model_module_version": "^0.20", + "state": { + "_model_module": "jupyter-leaflet", + "_model_module_version": "^0.20", + "_model_name": "LeafletDrawControlModel", + "_view_count": null, + "_view_module": "jupyter-leaflet", + "_view_module_version": "^0.20", + "_view_name": "LeafletDrawControlView", + "circle": {}, + "circlemarker": {}, + "data": [], + "edit": true, + "marker": { + "shapeOptions": { + "color": "#3388ff" + } + }, + "options": [ + "position" + ], + "polygon": { + "shapeOptions": {} + }, + "polyline": { + "shapeOptions": {} + }, + "position": "topleft", + "rectangle": { + "shapeOptions": { + "color": "#3388ff" + } + }, + "remove": true + } + }, + "c63675a6e3264b4aa1177102b7ee30d7": { + "model_module": "jupyter-leaflet", + "model_name": "LeafletScaleControlModel", + "model_module_version": "^0.20", + "state": { + "_model_module": "jupyter-leaflet", + "_model_module_version": "^0.20", + "_model_name": "LeafletScaleControlModel", + "_view_count": null, + "_view_module": "jupyter-leaflet", + "_view_module_version": "^0.20", + "_view_name": "LeafletScaleControlView", + "imperial": true, + "max_width": 100, + "metric": true, + "options": [ + "imperial", + "max_width", + "metric", + "position", + "update_when_idle" + ], + "position": "bottomleft", + "update_when_idle": false + } + }, + "bfaf542bd2514506aa5f675efa7cb807": { + "model_module": "jupyter-leaflet", + "model_name": "LeafletMeasureControlModel", + "model_module_version": "^0.20", + "state": { + "_custom_units": {}, + "_model_module": "jupyter-leaflet", + "_model_module_version": "^0.20", + "_model_name": "LeafletMeasureControlModel", + "_view_count": null, + "_view_module": "jupyter-leaflet", + "_view_module_version": "^0.20", + "_view_name": "LeafletMeasureControlView", + "active_color": "orange", + "capture_z_index": 10000, + "completed_color": "#C8F2BE", + "options": [ + "active_color", + "capture_z_index", + "completed_color", + "popup_options", + "position", + "primary_area_unit", + "primary_length_unit", + "secondary_area_unit", + "secondary_length_unit" + ], + "popup_options": { + "className": "leaflet-measure-resultpopup", + "autoPanPadding": [ + 10, + 10 + ] + }, + "position": "bottomleft", + "primary_area_unit": "acres", + "primary_length_unit": "kilometers", + "secondary_area_unit": null, + "secondary_length_unit": null + } + }, + "816db69faa5f403782a95534ae89e5b0": { + "model_module": "jupyter-leaflet", + "model_name": "LeafletAttributionControlModel", + "model_module_version": "^0.20", + "state": { + "_model_module": "jupyter-leaflet", + "_model_module_version": "^0.20", + "_model_name": "LeafletAttributionControlModel", + "_view_count": null, + "_view_module": "jupyter-leaflet", + "_view_module_version": "^0.20", + "_view_name": "LeafletAttributionControlView", + "options": [ + "position", + "prefix" + ], + "position": "bottomright", + "prefix": "ipyleaflet" + } + }, + "45e9f57280494b81bedec5a978abd60d": { + "model_module": "jupyter-leaflet", + "model_name": "LeafletMapStyleModel", + "model_module_version": "^0.20", + "state": { + "_model_module": "jupyter-leaflet", + "_model_module_version": "^0.20", + "_model_name": "LeafletMapStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "cursor": "grab" + } + }, + "3ce4cd936a65472285609d916a781836": { + "model_module": "jupyter-leaflet", + "model_name": "LeafletMapStyleModel", + "model_module_version": "^0.20", + "state": { + "_model_module": "jupyter-leaflet", + "_model_module_version": "^0.20", + "_model_name": "LeafletMapStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "cursor": "move" + } + }, + "2ede49326b5c40228d616aab967910e0": { + "model_module": "jupyter-leaflet", + "model_name": "LeafletTileLayerModel", + "model_module_version": "^0.20", + "state": { + "_model_module": "jupyter-leaflet", + "_model_module_version": "^0.20", + "_model_name": "LeafletTileLayerModel", + "_view_count": null, + "_view_module": "jupyter-leaflet", + "_view_module_version": "^0.20", + "_view_name": "LeafletTileLayerView", + "attribution": "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community", + "base": true, + "bottom": true, + "bounds": null, + "detect_retina": false, + "loading": false, + "max_native_zoom": null, + "max_zoom": 30, + "min_native_zoom": null, + "min_zoom": 1, + "name": "Esri.WorldImagery", + "no_wrap": false, + "opacity": 1, + "options": [ + "attribution", + "bounds", + "detect_retina", + "max_native_zoom", + "max_zoom", + "min_native_zoom", + "min_zoom", + "no_wrap", + "pm_ignore", + "tile_size", + "tms", + "zoom_offset" + ], + "pane": "", + "pm_ignore": true, + "popup": null, + "popup_max_height": null, + "popup_max_width": 300, + "popup_min_width": 50, + "show_loading": false, + "snap_ignore": true, + "subitems": [], + "tile_size": 256, + "tms": false, + "url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", + "visible": true, + "zoom_offset": 0 + } + }, + "ceae30f50513407ab97fb3321255b65e": { + "model_module": "jupyter-leaflet", + "model_name": "LeafletTileLayerModel", + "model_module_version": "^0.20", + "state": { + "_model_module": "jupyter-leaflet", + "_model_module_version": "^0.20", + "_model_name": "LeafletTileLayerModel", + "_view_count": null, + "_view_module": "jupyter-leaflet", + "_view_module_version": "^0.20", + "_view_name": "LeafletTileLayerView", + "attribution": "Google Earth Engine", + "base": false, + "bottom": true, + "bounds": null, + "detect_retina": false, + "loading": false, + "max_native_zoom": null, + "max_zoom": 24, + "min_native_zoom": null, + "min_zoom": 0, + "name": "25m DW Tree Cover", + "no_wrap": false, + "opacity": 1, + "options": [ + "attribution", + "bounds", + "detect_retina", + "max_native_zoom", + "max_zoom", + "min_native_zoom", + "min_zoom", + "no_wrap", + "pm_ignore", + "tile_size", + "tms", + "zoom_offset" + ], + "pane": "", + "pm_ignore": true, + "popup": null, + "popup_max_height": null, + "popup_max_width": 300, + "popup_min_width": 50, + "show_loading": false, + "snap_ignore": true, + "subitems": [], + "tile_size": 256, + "tms": false, + "url": "https://earthengine.googleapis.com/v1/projects/corestack-datasets-alpha/maps/0c7f61c2c8e9a1c1ddd6d497bc868f34-ac075ded404547da832abc6f7332bc4e/tiles/{z}/{x}/{y}", + "visible": true, + "zoom_offset": 0 + } + }, + "a3a057d1ddb0491bbbac924c775080ea": { + "model_module": "jupyter-leaflet", + "model_name": "LeafletTileLayerModel", + "model_module_version": "^0.20", + "state": { + "_model_module": "jupyter-leaflet", + "_model_module_version": "^0.20", + "_model_name": "LeafletTileLayerModel", + "_view_count": null, + "_view_module": "jupyter-leaflet", + "_view_module_version": "^0.20", + "_view_name": "LeafletTileLayerView", + "attribution": "Google Earth Engine", + "base": false, + "bottom": true, + "bounds": null, + "detect_retina": false, + "loading": false, + "max_native_zoom": null, + "max_zoom": 24, + "min_native_zoom": null, + "min_zoom": 0, + "name": "LTP-STP Image", + "no_wrap": false, + "opacity": 1, + "options": [ + "attribution", + "bounds", + "detect_retina", + "max_native_zoom", + "max_zoom", + "min_native_zoom", + "min_zoom", + "no_wrap", + "pm_ignore", + "tile_size", + "tms", + "zoom_offset" + ], + "pane": "", + "pm_ignore": true, + "popup": null, + "popup_max_height": null, + "popup_max_width": 300, + "popup_min_width": 50, + "show_loading": false, + "snap_ignore": true, + "subitems": [], + "tile_size": 256, + "tms": false, + "url": "https://earthengine.googleapis.com/v1/projects/corestack-datasets-alpha/maps/c4b980920fd4c2a0b768d12ccef47b01-af1a034eef46a65aa406908b7a0852e1/tiles/{z}/{x}/{y}", + "visible": true, + "zoom_offset": 0 + } + }, + "23acd2e7dfea4fb7891dd6241fe91626": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": "600px", + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "f70085e859494024a296847249f0019d": { + "model_module": "jupyter-leaflet", + "model_name": "LeafletMapStyleModel", + "model_module_version": "^0.20", + "state": { + "_model_module": "jupyter-leaflet", + "_model_module_version": "^0.20", + "_model_name": "LeafletMapStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "cursor": "grab" + } + }, + "4f56216671684026b4d7dc7131aaeced": { + "model_module": "@jupyter-widgets/controls", + "model_name": "GridBoxModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "GridBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "GridBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_6d191e6e3f2944c89e802cfaf2f4d973", + "IPY_MODEL_11573ba3646148968e821ccd3cc82ee3" + ], + "layout": "IPY_MODEL_08df80e9b6e94b60a1f7b30a1e96b6b8" + } + }, + "6ac4b53d3f9a477a8a50b91f3c1a78a1": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.map_widgets.SearchBar", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e7, o5) {\n if (this._$cssResult$ = true, o5 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e7;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e7 = void 0 !== s2 && 1 === s2.length;\n e7 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e7 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e7) => {\n const o5 = 1 === t3.length ? t3[0] : e7.reduce((e8, s2, o6) => e8 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o6 + 1], t3[0]);\n return new n(o5, t3, s);\n};\nvar S = (s2, o5) => {\n if (e) s2.adoptedStyleSheets = o5.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e7 of o5) {\n const o6 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o6.setAttribute(\"nonce\", n5), o6.textContent = e7.cssText, s2.appendChild(o6);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e7 = \"\";\n for (const s2 of t4.cssRules) e7 += s2.cssText;\n return r(e7);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r5 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r5 && e2(this.prototype, t3, r5);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e7, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e7?.call(this);\n }, set(s3) {\n const r5 = e7?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r5, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e7 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e7) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e7 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e7 && true === i4.reflect) {\n const r5 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r5 ? this.removeAttribute(e7) : this.setAttribute(e7, r5), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e7 = i4._$Eh.get(t3);\n if (void 0 !== e7 && this._$Em !== e7) {\n const t4 = i4.getPropertyOptions(e7), r5 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e7, this[e7] = r5.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e7 = [];\n let h4, o5 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r5, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r5 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o5 += n5 === T ? s3 + _ : c4 >= 0 ? (e7.push(r5), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o5 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e7];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e7;\n this.parts = [];\n let h4 = 0, o5 = 0;\n const n5 = t3.length - 1, r5 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e7 = I.nextNode()) && r5.length < n5; ) {\n if (1 === e7.nodeType) {\n if (e7.hasAttributes()) for (const t4 of e7.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o5++], s3 = e7.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r5.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e7.removeAttribute(t4);\n } else t4.startsWith(v) && (r5.push({ type: 6, index: h4 }), e7.removeAttribute(t4));\n if (M.test(e7.tagName)) {\n const t4 = e7.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e7.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e7.append(t4[s3], lt()), I.nextNode(), r5.push({ type: 2, index: ++h4 });\n e7.append(t4[i5], lt());\n }\n }\n } else if (8 === e7.nodeType) if (e7.data === m) r5.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e7.data.indexOf(v, t4 + 1)); ) r5.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e7) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e7 ? s2.o?.[e7] : s2.l;\n const o5 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o5 && (h4?._$AO?.(false), void 0 === o5 ? h4 = void 0 : (h4 = new o5(t3), h4._$AT(t3, s2, e7)), void 0 !== e7 ? (s2.o ??= [])[e7] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e7)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e7 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e7;\n let h4 = I.nextNode(), o5 = 0, n5 = 0, r5 = s2[0];\n for (; void 0 !== r5; ) {\n if (o5 === r5.index) {\n let i5;\n 2 === r5.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r5.type ? i5 = new r5.ctor(h4, r5.name, r5.strings, this, t3) : 6 === r5.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r5 = s2[++n5];\n }\n o5 !== r5?.index && (h4 = I.nextNode(), o5++);\n }\n return I.currentNode = w, e7;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e7) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e7, this.v = e7?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e7 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e7) this._$AH.p(i4);\n else {\n const t4 = new F(e7, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e7 = 0;\n for (const h4 of t3) e7 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e7], s2._$AI(h4), e7++;\n e7 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e7), i4.length = e7);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e7, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e7, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e7) {\n const h4 = this.strings;\n let o5 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o5 = !st(t3) || t3 !== this._$AH && t3 !== R, o5 && (this._$AH = t3);\n else {\n const e8 = t3;\n let n5, r5;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r5 = z(this, e8[s2 + n5], i4, n5), r5 === R && (r5 = this._$AH[n5]), o5 ||= !st(r5) || r5 !== this._$AH[n5], r5 === D ? t3 = D : t3 !== D && (t3 += (r5 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r5;\n }\n o5 && !e7 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e7, h4) {\n super(t3, i4, s2, e7, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e7 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e7);\n e7 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e7 = s2?.renderBefore ?? i4;\n let h4 = e7._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e7._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e7 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e7, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e7, r5) => {\n const { kind: n5, metadata: i4 } = r5;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r5.name, t3), \"accessor\" === n5) {\n const { name: o5 } = r5;\n return { set(r6) {\n const n6 = e7.get.call(this);\n e7.set.call(this, r6), this.requestUpdate(o5, n6, t3);\n }, init(e8) {\n return void 0 !== e8 && this.P(o5, void 0, t3), e8;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o5 } = r5;\n return function(r6) {\n const n6 = this[o5];\n e7.call(this, r6), this.requestUpdate(o5, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e7, o5) => \"object\" == typeof o5 ? r3(t3, e7, o5) : ((t4, e8, o6) => {\n const r5 = e8.hasOwnProperty(o6);\n return e8.constructor.createProperty(o6, r5 ? { ...t4, wrapped: true } : t4), r5 ? Object.getOwnPropertyDescriptor(e8, o6) : void 0;\n })(t3, e7, o5);\n}\n\n// node_modules/@lit/reactive-element/decorators/base.js\nvar e3 = (e7, t3, c4) => (c4.configurable = true, c4.enumerable = true, Reflect.decorate && \"object\" != typeof t3 && Object.defineProperty(e7, t3, c4), c4);\n\n// node_modules/@lit/reactive-element/decorators/query.js\nfunction e4(e7, r5) {\n return (n5, s2, i4) => {\n const o5 = (t3) => t3.renderRoot?.querySelector(e7) ?? null;\n if (r5) {\n const { get: e8, set: r6 } = \"object\" == typeof s2 ? n5 : i4 ?? (() => {\n const t3 = Symbol();\n return { get() {\n return this[t3];\n }, set(e9) {\n this[t3] = e9;\n } };\n })();\n return e3(n5, s2, { get() {\n let t3 = e8.call(this);\n return void 0 === t3 && (t3 = o5(this), (null !== t3 || this.hasUpdated) && r6.call(this, t3)), t3;\n } });\n }\n return e3(n5, s2, { get() {\n return o5(this);\n } });\n };\n}\n\n// node_modules/@lit/reactive-element/decorators/query-all.js\nvar e5;\nfunction r4(r5) {\n return (n5, o5) => e3(n5, o5, { get() {\n return (this.renderRoot ?? (e5 ??= document.createDocumentFragment())).querySelectorAll(r5);\n } });\n}\n\n// node_modules/@lit/reactive-element/decorators/query-assigned-elements.js\nfunction o4(o5) {\n return (e7, n5) => {\n const { slot: r5, selector: s2 } = o5 ?? {}, c4 = \"slot\" + (r5 ? `[name=${r5}]` : \":not([name])\");\n return e3(e7, n5, { get() {\n const t3 = this.renderRoot?.querySelector(c4), e8 = t3?.assignedElements(o5) ?? [];\n return void 0 === s2 ? e8 : e8.filter((t4) => t4.matches(s2));\n } });\n };\n}\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e6 = (t3) => (...e7) => ({ _$litDirective$: t3, values: e7 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e7, i4) {\n this.t = t3, this._$AM = e7, this.i = i4;\n }\n _$AS(t3, e7) {\n return this.update(t3, e7);\n }\n update(t3, e7) {\n return this.render(...e7);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e6(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r5 = !!s2[t4];\n r5 === this.st.has(t4) || this.nt?.has(t4) || (r5 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// node_modules/lit-html/directives/style-map.js\nvar ee = \"important\";\nvar ie = \" !\" + ee;\nvar se = e6(class extends i3 {\n constructor(e7) {\n if (super(e7), e7.type !== t2.ATTRIBUTE || \"style\" !== e7.name || e7.strings?.length > 2) throw Error(\"The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return Object.keys(t3).reduce((e7, r5) => {\n const s2 = t3[r5];\n return null == s2 ? e7 : e7 + `${r5 = r5.includes(\"-\") ? r5 : r5.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g, \"-$&\").toLowerCase()}:${s2};`;\n }, \"\");\n }\n update(t3, [e7]) {\n const { style: r5 } = t3.element;\n if (void 0 === this.ft) return this.ft = new Set(Object.keys(e7)), this.render(e7);\n for (const t4 of this.ft) null == e7[t4] && (this.ft.delete(t4), t4.includes(\"-\") ? r5.removeProperty(t4) : r5[t4] = null);\n for (const t4 in e7) {\n const s2 = e7[t4];\n if (null != s2) {\n this.ft.add(t4);\n const e8 = \"string\" == typeof s2 && s2.endsWith(ie);\n t4.includes(\"-\") || e8 ? r5.setProperty(t4, e8 ? s2.slice(0, -11) : s2, e8 ? ee : \"\") : r5[t4] = s2;\n }\n }\n return R;\n }\n});\n\n// js/tab_panel.ts\nfunction convertToId(name) {\n return (name || \"\").trim().replace(\" \", \"-\").toLowerCase();\n}\nvar TabPanel = class extends h3 {\n constructor() {\n super(...arguments);\n this.tabs = [];\n this.index = 0;\n this.mode = 1 /* HIDE_ON_SECOND_CLICK */;\n this.alignment = \"left\" /* LEFT */;\n }\n static get componentName() {\n return `tab-panel`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n ::slotted(*) {\n display: none;\n }\n\n ::slotted(.show-tab) {\n display: block;\n }\n\n .container {\n padding: 0;\n width: 100%;\n }\n\n .tab-container {\n align-items: center;\n display: flex;\n flex-direction: row;\n }\n\n .tab-container.center {\n justify-content: center;\n }\n\n .tab-container.left {\n justify-content: flex-start;\n }\n\n .tab-container.right {\n justify-content: flex-end;\n }\n\n .tab-container button {\n border-radius: 5px;\n height: 28px;\n margin: 2px 0 2px 8px;\n user-select: none;\n }\n\n .tab-container button.icon {\n font-size: 16px;\n width: 28px;\n }\n\n .tab-container button.name {\n padding: 0 8px;\n }\n `\n ];\n }\n render() {\n return ke`\n
\n \n ${this.renderTabs()}\n
\n \n \n `;\n }\n update(changedProperties) {\n super.update(changedProperties);\n if (changedProperties.has(\"index\") && changedProperties.get(\"index\") != null) {\n this.updateSlotChildren();\n }\n }\n updateSlotChildren() {\n if (!this.tabContentElements) {\n return;\n }\n this.tabContentElements.forEach((element, i4) => {\n element.classList.remove(\"show-tab\");\n const id = convertToId(this.tabs[i4].name);\n element.setAttribute(\"id\", `tabpanel-${id}-${i4}`);\n element.setAttribute(\"role\", \"tabpanel\");\n element.setAttribute(\"aria-labelledby\", `tab-${id}-${i4}`);\n });\n this.tabContentElements[this.index]?.classList.add(\"show-tab\");\n }\n renderTabs() {\n return this.tabs.map((tab, i4) => {\n const id = convertToId(this.tabs[i4].name);\n return ke` {\n this.onTabClick(i4);\n }}>\n ${tab.icon ? ke`${tab.icon}` : D}\n ${tab.name}\n `;\n });\n }\n onTabClick(index) {\n switch (this.mode) {\n case 1 /* HIDE_ON_SECOND_CLICK */:\n this.index = this.index === index ? -1 : index;\n break;\n case 0 /* ALWAYS_SHOW */:\n default:\n this.index = index;\n }\n this.dispatchEvent(new CustomEvent(\"tab-changed\", {\n detail: index\n }));\n }\n};\n__decorateClass([\n n4({ type: Array })\n], TabPanel.prototype, \"tabs\", 2);\n__decorateClass([\n n4({ type: Number })\n], TabPanel.prototype, \"index\", 2);\n__decorateClass([\n n4({ type: Number })\n], TabPanel.prototype, \"mode\", 2);\n__decorateClass([\n n4({ type: String })\n], TabPanel.prototype, \"alignment\", 2);\n__decorateClass([\n r4(\".tab\")\n], TabPanel.prototype, \"tabElements\", 2);\n__decorateClass([\n o4()\n], TabPanel.prototype, \"tabContentElements\", 2);\nif (!customElements.get(TabPanel.componentName)) {\n customElements.define(TabPanel.componentName, TabPanel);\n}\n\n// node_modules/lit-html/directives/unsafe-html.js\nvar le = class extends i3 {\n constructor(i4) {\n if (super(i4), this.it = D, i4.type !== t2.CHILD) throw Error(this.constructor.directiveName + \"() can only be used in child bindings\");\n }\n render(t3) {\n if (t3 === D || null == t3) return this._t = void 0, this.it = t3;\n if (t3 === R) return t3;\n if (\"string\" != typeof t3) throw Error(this.constructor.directiveName + \"() called with a non-string value\");\n if (t3 === this.it) return this._t;\n this.it = t3;\n const i4 = [t3];\n return i4.raw = i4, this._t = { _$litType$: this.constructor.resultType, strings: i4, values: [] };\n }\n};\nle.directiveName = \"unsafeHTML\", le.resultType = 1;\nvar ae = e6(le);\n\n// js/container.ts\nvar Container = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.icon = \"\";\n this.title = \"\";\n this.collapsed = false;\n this.hideCloseButton = false;\n this.compactMode = false;\n this.noHeader = false;\n this.reverseHeader = false;\n }\n static get componentName() {\n return `widget-container`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n .container {\n background: var(--jp-layout-color1);\n border-radius: 4.5px;\n box-shadow: 4px 5px 8px 0px #9e9e9e;\n }\n\n div {\n background-color: var(--colab-primary-surface-color, --jp-layout-color1, white);\n }\n\n .header {\n display: flex;\n gap: 4px;\n padding: 4px;\n }\n\n .reversed {\n flex-direction: row-reverse;\n }\n\n .icon {\n align-items: center;\n display: flex;\n font-size: 20px;\n height: 28px;\n justify-content: center;\n padding: 0 4px;\n }\n\n .widget-container {\n padding: 8px 12px 12px 12px;\n }\n\n .hidden {\n display: none;\n }\n\n .header-button {\n font-size: 16px;\n height: 28px;\n width: 28px;\n }\n\n .compact-header-button {\n background: transparent;\n font-size: 16px;\n height: 28px;\n width: 28px;\n }\n\n .header-text {\n align-content: center;\n flex-grow: 1;\n padding: 0 12px 0 0;\n }\n\n .left-padding {\n padding-left: 8px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"icon\", \"icon\"],\n [\"collapsed\", \"collapsed\"],\n [\"title\", \"title\"],\n [\"hide_close_button\", \"hideCloseButton\"]\n ]);\n }\n render() {\n return ke`\n
\n ${this.noHeader ? D : this.renderHeader()}\n
\n \n
\n
\n `;\n }\n renderHeader() {\n return this.compactMode ? this.renderCompactHeader() : ke`\n
\n ${this.renderIcon()}\n ${this.title ? this.renderTitle() : D}\n ${this.renderCollapseButton()}\n ${this.renderCloseButton()}\n
`;\n }\n renderCompactHeader() {\n return ke`
\n ${this.renderCollapseButton()}\n ${this.title && !this.collapsed ? this.renderTitle() : D}\n ${this.renderCloseButton()}\n
`;\n }\n renderCloseButton() {\n if (this.hideCloseButton) {\n return D;\n }\n return ke`\n \n \n \n `;\n }\n renderTitle() {\n return ke`${this.title}`;\n }\n onCloseButtonClicked() {\n this.dispatchEvent(new CustomEvent(\"close-clicked\", {}));\n }\n onCollapseToggled() {\n this.collapsed = !this.collapsed;\n this.dispatchEvent(new CustomEvent(\"collapse-clicked\", {}));\n }\n renderIcon() {\n return ke`\n ${this.icon}\n `;\n }\n renderCollapseButton() {\n let icon;\n if (this.compactMode) {\n icon = this.renderIcon();\n } else if (this.collapsed) {\n icon = ke``;\n } else {\n icon = ke``;\n }\n return ke`\n ${icon}\n `;\n }\n};\n__decorateClass([\n n4({ type: String })\n], Container.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], Container.prototype, \"title\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], Container.prototype, \"collapsed\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], Container.prototype, \"hideCloseButton\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], Container.prototype, \"compactMode\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], Container.prototype, \"noHeader\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], Container.prototype, \"reverseHeader\", 2);\nif (!customElements.get(Container.componentName)) {\n customElements.define(Container.componentName, Container);\n}\n\n// js/search_bar.ts\nvar SearchBar = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.collapsed = true;\n this.tab_index = 0;\n this.locationModel = JSON.stringify({\n search: \"\",\n results: [],\n selected: \"\",\n additional_html: \"\"\n });\n this.datasetModel = JSON.stringify({\n search: \"\",\n results: [],\n selected: \"\",\n additional_html: \"\"\n });\n }\n static get componentName() {\n return `search-bar`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n .row {\n display: flex;\n gap: 6px;\n }\n\n .input-container {\n max-width: 320px;\n }\n\n .input-container > p {\n margin: 8px 3px;\n }\n\n input.search {\n margin: 2px 2px 8px 2px;\n width: calc(100% - 4px);\n }\n\n ul.results {\n list-style-type: none;\n margin: 0;\n margin-bottom: 4px;\n padding: 8px 0;\n }\n\n label.result {\n align-items: center;\n display: flex;\n margin-bottom: 4px;\n }\n\n .import-button, .reset-button {\n margin: 0 2px 2px 2px;\n padding: 0 8px;\n white-space: nowrap;\n }\n\n .dataset-select {\n margin-bottom: 2px;\n margin-right: 2px;\n }\n\n .additional-html-container {\n max-height: 300px;\n overflow: auto;\n padding: 8px 0;\n }\n\n .additional-html-container pre {\n white-space: break-spaces;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"collapsed\", \"collapsed\"],\n [\"tab_index\", \"tab_index\"],\n [\"location_model\", \"locationModel\"],\n [\"dataset_model\", \"datasetModel\"]\n ]);\n }\n render() {\n return ke`\n \n {\n this.tab_index = e7.detail;\n }}\n .mode=\"${0 /* ALWAYS_SHOW */}\">\n
\n ${this.renderLocationSearch()}\n
\n
\n ${this.renderDatasetSearch()}\n
\n \n `;\n }\n renderLocationSearch() {\n const locationModel = JSON.parse(this.locationModel);\n const helpText = ke`

\n Find your point of interest (by place name,\n address, or coordinates, e.g. 40,-100)\n

`;\n const searchInput = ke` {\n if (e7.key === \"Enter\") {\n e7.preventDefault();\n const locationModel2 = JSON.parse(this.locationModel);\n locationModel2.search = this.locationSearch.value || \"\";\n this.locationModel = JSON.stringify(locationModel2);\n }\n }}\" />`;\n const renderedInputs = [helpText, searchInput];\n if (locationModel.results.length) {\n const results = ke`\n ${locationModel.results.map((result) => ke`\n
  • \n \n
  • `)}\n `;\n renderedInputs.push(ke`
      \n ${results}\n
    `);\n }\n if (locationModel.additional_html) {\n renderedInputs.push(ke`
    \n ${ae(locationModel.additional_html)}\n
    `);\n }\n if (locationModel.search || locationModel.results.length || locationModel.selected) {\n renderedInputs.push(ke` {\n this.locationModel = JSON.stringify({\n search: \"\",\n results: [],\n selected: \"\",\n additional_html: \"\"\n });\n if (this.locationSearch) {\n this.locationSearch.value = \"\";\n }\n }}\">Reset`);\n }\n return renderedInputs;\n }\n renderDatasetSearch() {\n const datasetModel = JSON.parse(this.datasetModel);\n const helpText = ke`

    \n Find a dataset by GEE data catalog name or keywords, e.g. elevation\n

    `;\n const searchInput = ke` {\n if (e7.key === \"Enter\") {\n e7.preventDefault();\n const datasetModel2 = JSON.parse(this.datasetModel);\n datasetModel2.search = this.datasetSearch?.value || \"\";\n this.datasetModel = JSON.stringify(datasetModel2);\n }\n }}\" />`;\n const renderedInputs = [helpText, searchInput];\n const importButton = ke` {\n this.model?.send({ type: \"click\", id: \"import\" });\n }}\">\n Reveal Code\n `;\n const results = ke`\n {\n const input = e7.target;\n const datasetModel2 = JSON.parse(this.datasetModel);\n datasetModel2.selected = input.value || \"\";\n this.datasetModel = JSON.stringify(datasetModel2);\n }}\">\n ${datasetModel.results.map((result) => ke`\n \n `)}\n \n `;\n renderedInputs.push(\n ke`
    \n ${importButton}\n ${results}\n
    `\n );\n if (datasetModel.additional_html) {\n renderedInputs.push(ke`
    \n ${ae(datasetModel.additional_html)}\n
    `);\n }\n return renderedInputs;\n }\n};\n__decorateClass([\n n4()\n], SearchBar.prototype, \"collapsed\", 2);\n__decorateClass([\n n4()\n], SearchBar.prototype, \"tab_index\", 2);\n__decorateClass([\n n4()\n], SearchBar.prototype, \"locationModel\", 2);\n__decorateClass([\n n4()\n], SearchBar.prototype, \"datasetModel\", 2);\n__decorateClass([\n e4(\".location-search\")\n], SearchBar.prototype, \"locationSearch\", 2);\n__decorateClass([\n r4(\".location-results input\")\n], SearchBar.prototype, \"locationResults\", 2);\n__decorateClass([\n e4(\".dataset-search\")\n], SearchBar.prototype, \"datasetSearch\", 2);\nif (!customElements.get(SearchBar.componentName)) {\n customElements.define(SearchBar.componentName, SearchBar);\n}\nasync function render({ model, el }) {\n loadFonts();\n const row = document.createElement(SearchBar.componentName);\n row.model = model;\n el.appendChild(row);\n}\nvar search_bar_default = { render };\nexport {\n SearchBar,\n search_bar_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/style-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/unsafe-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "collapsed": true, + "dataset_model": "{\"search\": \"\", \"results\": [], \"selected\": \"\", \"additional_html\": \"\"}", + "layout": "IPY_MODEL_0164447f09534c85a9bc9ba3b0a8ced5", + "location_model": "{\"search\": \"\", \"results\": [], \"selected\": \"\", \"additional_html\": \"\"}", + "tab_index": 0 + } + }, + "6d191e6e3f2944c89e802cfaf2f4d973": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.map_widgets.LayerManager", + "_dom_classes": [], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nasync function unpackModels(modelIds, manager) {\n return Promise.all(\n modelIds.map((id) => manager.get_model(id.slice(\"IPY_MODEL_\".length)))\n );\n}\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nasync function updateChildren(container, model, property = \"children\") {\n let children = model.get(property);\n if (!Array.isArray(children)) {\n children = [children];\n }\n const child_models = await unpackModels(children, model.widget_manager);\n const child_views = await Promise.all(\n child_models.map((model2) => model2.widget_manager.create_view(model2))\n );\n container.innerHTML = ``;\n for (const child_view of child_views) {\n container.appendChild(child_view.el);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/container.ts\nvar Container = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.icon = \"\";\n this.title = \"\";\n this.collapsed = false;\n this.hideCloseButton = false;\n this.compactMode = false;\n this.noHeader = false;\n this.reverseHeader = false;\n }\n static get componentName() {\n return `widget-container`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n .container {\n background: var(--jp-layout-color1);\n border-radius: 4.5px;\n box-shadow: 4px 5px 8px 0px #9e9e9e;\n }\n\n div {\n background-color: var(--colab-primary-surface-color, --jp-layout-color1, white);\n }\n\n .header {\n display: flex;\n gap: 4px;\n padding: 4px;\n }\n\n .reversed {\n flex-direction: row-reverse;\n }\n\n .icon {\n align-items: center;\n display: flex;\n font-size: 20px;\n height: 28px;\n justify-content: center;\n padding: 0 4px;\n }\n\n .widget-container {\n padding: 8px 12px 12px 12px;\n }\n\n .hidden {\n display: none;\n }\n\n .header-button {\n font-size: 16px;\n height: 28px;\n width: 28px;\n }\n\n .compact-header-button {\n background: transparent;\n font-size: 16px;\n height: 28px;\n width: 28px;\n }\n\n .header-text {\n align-content: center;\n flex-grow: 1;\n padding: 0 12px 0 0;\n }\n\n .left-padding {\n padding-left: 8px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"icon\", \"icon\"],\n [\"collapsed\", \"collapsed\"],\n [\"title\", \"title\"],\n [\"hide_close_button\", \"hideCloseButton\"]\n ]);\n }\n render() {\n return ke`\n
    \n ${this.noHeader ? D : this.renderHeader()}\n
    \n \n
    \n
    \n `;\n }\n renderHeader() {\n return this.compactMode ? this.renderCompactHeader() : ke`\n
    \n ${this.renderIcon()}\n ${this.title ? this.renderTitle() : D}\n ${this.renderCollapseButton()}\n ${this.renderCloseButton()}\n
    `;\n }\n renderCompactHeader() {\n return ke`
    \n ${this.renderCollapseButton()}\n ${this.title && !this.collapsed ? this.renderTitle() : D}\n ${this.renderCloseButton()}\n
    `;\n }\n renderCloseButton() {\n if (this.hideCloseButton) {\n return D;\n }\n return ke`\n \n \n \n `;\n }\n renderTitle() {\n return ke`${this.title}`;\n }\n onCloseButtonClicked() {\n this.dispatchEvent(new CustomEvent(\"close-clicked\", {}));\n }\n onCollapseToggled() {\n this.collapsed = !this.collapsed;\n this.dispatchEvent(new CustomEvent(\"collapse-clicked\", {}));\n }\n renderIcon() {\n return ke`\n ${this.icon}\n `;\n }\n renderCollapseButton() {\n let icon;\n if (this.compactMode) {\n icon = this.renderIcon();\n } else if (this.collapsed) {\n icon = ke``;\n } else {\n icon = ke``;\n }\n return ke`\n ${icon}\n `;\n }\n};\n__decorateClass([\n n4({ type: String })\n], Container.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], Container.prototype, \"title\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], Container.prototype, \"collapsed\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], Container.prototype, \"hideCloseButton\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], Container.prototype, \"compactMode\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], Container.prototype, \"noHeader\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], Container.prototype, \"reverseHeader\", 2);\nif (!customElements.get(Container.componentName)) {\n customElements.define(Container.componentName, Container);\n}\n\n// js/layer_manager.ts\nvar LayerManager = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.visible = false;\n this.tabIndex = 0;\n this.collapsed = true;\n }\n static get componentName() {\n return `layer-manager`;\n }\n static {\n this.styles = [\n legacyStyles,\n i`\n .row {\n align-items: center;\n display: flex;\n gap: 4px;\n height: 28px;\n }\n\n .visibility-checkbox {\n margin: 2px;\n }\n\n .layer-manager-rows {\n display: flex;\n flex-direction: column;\n gap: 10px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"children\", null],\n [\"visible\", \"visible\"]\n ]);\n }\n render() {\n return ke`\n \n
    \n
    \n
    \n \n \n All layers on/off\n
    \n \n
    \n
    \n \n `;\n }\n onLayerVisibilityChanged(_event) {\n this.visible = !this.visible;\n }\n};\n__decorateClass([\n n4()\n], LayerManager.prototype, \"visible\", 2);\n__decorateClass([\n n4()\n], LayerManager.prototype, \"tabIndex\", 2);\n__decorateClass([\n n4()\n], LayerManager.prototype, \"collapsed\", 2);\nif (!customElements.get(LayerManager.componentName)) {\n customElements.define(LayerManager.componentName, LayerManager);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(LayerManager.componentName);\n manager.model = model;\n el.appendChild(manager);\n updateChildren(manager, model);\n model.on(\"change:children\", () => {\n updateChildren(manager, model);\n });\n}\nvar layer_manager_default = { render };\nexport {\n LayerManager,\n layer_manager_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "children": [ + "IPY_MODEL_d6061d8c11874c50a4379be53a5e3029", + "IPY_MODEL_0f756d3b935c4b14a4aca5d85723c84d", + "IPY_MODEL_d25e876b06b84a57a476a14ce924b60a" + ], + "layout": "IPY_MODEL_a0a9213c4e3e464287da4bb6517408e5", + "visible": true + } + }, + "11573ba3646148968e821ccd3cc82ee3": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.Toolbar", + "_dom_classes": [], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e6, o5) {\n if (this._$cssResult$ = true, o5 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e6;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e6 = void 0 !== s2 && 1 === s2.length;\n e6 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e6 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e6) => {\n const o5 = 1 === t3.length ? t3[0] : e6.reduce((e7, s2, o6) => e7 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o6 + 1], t3[0]);\n return new n(o5, t3, s);\n};\nvar S = (s2, o5) => {\n if (e) s2.adoptedStyleSheets = o5.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e6 of o5) {\n const o6 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o6.setAttribute(\"nonce\", n5), o6.textContent = e6.cssText, s2.appendChild(o6);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e6 = \"\";\n for (const s2 of t4.cssRules) e6 += s2.cssText;\n return r(e6);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r5 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r5 && e2(this.prototype, t3, r5);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e6, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e6?.call(this);\n }, set(s3) {\n const r5 = e6?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r5, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e6 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e6) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e6 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e6 && true === i4.reflect) {\n const r5 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r5 ? this.removeAttribute(e6) : this.setAttribute(e6, r5), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e6 = i4._$Eh.get(t3);\n if (void 0 !== e6 && this._$Em !== e6) {\n const t4 = i4.getPropertyOptions(e6), r5 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e6, this[e6] = r5.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e6 = [];\n let h4, o5 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r5, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r5 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o5 += n5 === T ? s3 + _ : c4 >= 0 ? (e6.push(r5), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o5 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e6];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e6;\n this.parts = [];\n let h4 = 0, o5 = 0;\n const n5 = t3.length - 1, r5 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e6 = I.nextNode()) && r5.length < n5; ) {\n if (1 === e6.nodeType) {\n if (e6.hasAttributes()) for (const t4 of e6.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o5++], s3 = e6.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r5.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e6.removeAttribute(t4);\n } else t4.startsWith(v) && (r5.push({ type: 6, index: h4 }), e6.removeAttribute(t4));\n if (M.test(e6.tagName)) {\n const t4 = e6.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e6.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e6.append(t4[s3], lt()), I.nextNode(), r5.push({ type: 2, index: ++h4 });\n e6.append(t4[i5], lt());\n }\n }\n } else if (8 === e6.nodeType) if (e6.data === m) r5.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e6.data.indexOf(v, t4 + 1)); ) r5.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e6) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e6 ? s2.o?.[e6] : s2.l;\n const o5 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o5 && (h4?._$AO?.(false), void 0 === o5 ? h4 = void 0 : (h4 = new o5(t3), h4._$AT(t3, s2, e6)), void 0 !== e6 ? (s2.o ??= [])[e6] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e6)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e6 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e6;\n let h4 = I.nextNode(), o5 = 0, n5 = 0, r5 = s2[0];\n for (; void 0 !== r5; ) {\n if (o5 === r5.index) {\n let i5;\n 2 === r5.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r5.type ? i5 = new r5.ctor(h4, r5.name, r5.strings, this, t3) : 6 === r5.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r5 = s2[++n5];\n }\n o5 !== r5?.index && (h4 = I.nextNode(), o5++);\n }\n return I.currentNode = w, e6;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e6) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e6, this.v = e6?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e6 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e6) this._$AH.p(i4);\n else {\n const t4 = new F(e6, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e6 = 0;\n for (const h4 of t3) e6 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e6], s2._$AI(h4), e6++;\n e6 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e6), i4.length = e6);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e6, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e6, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e6) {\n const h4 = this.strings;\n let o5 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o5 = !st(t3) || t3 !== this._$AH && t3 !== R, o5 && (this._$AH = t3);\n else {\n const e7 = t3;\n let n5, r5;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r5 = z(this, e7[s2 + n5], i4, n5), r5 === R && (r5 = this._$AH[n5]), o5 ||= !st(r5) || r5 !== this._$AH[n5], r5 === D ? t3 = D : t3 !== D && (t3 += (r5 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r5;\n }\n o5 && !e6 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e6, h4) {\n super(t3, i4, s2, e6, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e6 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e6);\n e6 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e6 = s2?.renderBefore ?? i4;\n let h4 = e6._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e6._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e6 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e6, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e6, r5) => {\n const { kind: n5, metadata: i4 } = r5;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r5.name, t3), \"accessor\" === n5) {\n const { name: o5 } = r5;\n return { set(r6) {\n const n6 = e6.get.call(this);\n e6.set.call(this, r6), this.requestUpdate(o5, n6, t3);\n }, init(e7) {\n return void 0 !== e7 && this.P(o5, void 0, t3), e7;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o5 } = r5;\n return function(r6) {\n const n6 = this[o5];\n e6.call(this, r6), this.requestUpdate(o5, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e6, o5) => \"object\" == typeof o5 ? r3(t3, e6, o5) : ((t4, e7, o6) => {\n const r5 = e7.hasOwnProperty(o6);\n return e7.constructor.createProperty(o6, r5 ? { ...t4, wrapped: true } : t4), r5 ? Object.getOwnPropertyDescriptor(e7, o6) : void 0;\n })(t3, e6, o5);\n}\n\n// node_modules/@lit/reactive-element/decorators/base.js\nvar e3 = (e6, t3, c4) => (c4.configurable = true, c4.enumerable = true, Reflect.decorate && \"object\" != typeof t3 && Object.defineProperty(e6, t3, c4), c4);\n\n// node_modules/@lit/reactive-element/decorators/query-all.js\nvar e4;\nfunction r4(r5) {\n return (n5, o5) => e3(n5, o5, { get() {\n return (this.renderRoot ?? (e4 ??= document.createDocumentFragment())).querySelectorAll(r5);\n } });\n}\n\n// node_modules/@lit/reactive-element/decorators/query-assigned-elements.js\nfunction o4(o5) {\n return (e6, n5) => {\n const { slot: r5, selector: s2 } = o5 ?? {}, c4 = \"slot\" + (r5 ? `[name=${r5}]` : \":not([name])\");\n return e3(e6, n5, { get() {\n const t3 = this.renderRoot?.querySelector(c4), e7 = t3?.assignedElements(o5) ?? [];\n return void 0 === s2 ? e7 : e7.filter((t4) => t4.matches(s2));\n } });\n };\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e5 = (t3) => (...e6) => ({ _$litDirective$: t3, values: e6 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e6, i4) {\n this.t = t3, this._$AM = e6, this.i = i4;\n }\n _$AS(t3, e6) {\n return this.update(t3, e6);\n }\n update(t3, e6) {\n return this.render(...e6);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e5(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r5 = !!s2[t4];\n r5 === this.st.has(t4) || this.nt?.has(t4) || (r5 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nasync function unpackModels(modelIds, manager) {\n return Promise.all(\n modelIds.map((id) => manager.get_model(id.slice(\"IPY_MODEL_\".length)))\n );\n}\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nasync function updateChildren(container, model, property = \"children\") {\n let children = model.get(property);\n if (!Array.isArray(children)) {\n children = [children];\n }\n const child_models = await unpackModels(children, model.widget_manager);\n const child_views = await Promise.all(\n child_models.map((model2) => model2.widget_manager.create_view(model2))\n );\n container.innerHTML = ``;\n for (const child_view of child_views) {\n container.appendChild(child_view.el);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/container.ts\nvar Container = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.icon = \"\";\n this.title = \"\";\n this.collapsed = false;\n this.hideCloseButton = false;\n this.compactMode = false;\n this.noHeader = false;\n this.reverseHeader = false;\n }\n static get componentName() {\n return `widget-container`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n .container {\n background: var(--jp-layout-color1);\n border-radius: 4.5px;\n box-shadow: 4px 5px 8px 0px #9e9e9e;\n }\n\n div {\n background-color: var(--colab-primary-surface-color, --jp-layout-color1, white);\n }\n\n .header {\n display: flex;\n gap: 4px;\n padding: 4px;\n }\n\n .reversed {\n flex-direction: row-reverse;\n }\n\n .icon {\n align-items: center;\n display: flex;\n font-size: 20px;\n height: 28px;\n justify-content: center;\n padding: 0 4px;\n }\n\n .widget-container {\n padding: 8px 12px 12px 12px;\n }\n\n .hidden {\n display: none;\n }\n\n .header-button {\n font-size: 16px;\n height: 28px;\n width: 28px;\n }\n\n .compact-header-button {\n background: transparent;\n font-size: 16px;\n height: 28px;\n width: 28px;\n }\n\n .header-text {\n align-content: center;\n flex-grow: 1;\n padding: 0 12px 0 0;\n }\n\n .left-padding {\n padding-left: 8px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"icon\", \"icon\"],\n [\"collapsed\", \"collapsed\"],\n [\"title\", \"title\"],\n [\"hide_close_button\", \"hideCloseButton\"]\n ]);\n }\n render() {\n return ke`\n
    \n ${this.noHeader ? D : this.renderHeader()}\n
    \n \n
    \n
    \n `;\n }\n renderHeader() {\n return this.compactMode ? this.renderCompactHeader() : ke`\n
    \n ${this.renderIcon()}\n ${this.title ? this.renderTitle() : D}\n ${this.renderCollapseButton()}\n ${this.renderCloseButton()}\n
    `;\n }\n renderCompactHeader() {\n return ke`
    \n ${this.renderCollapseButton()}\n ${this.title && !this.collapsed ? this.renderTitle() : D}\n ${this.renderCloseButton()}\n
    `;\n }\n renderCloseButton() {\n if (this.hideCloseButton) {\n return D;\n }\n return ke`\n \n \n \n `;\n }\n renderTitle() {\n return ke`${this.title}`;\n }\n onCloseButtonClicked() {\n this.dispatchEvent(new CustomEvent(\"close-clicked\", {}));\n }\n onCollapseToggled() {\n this.collapsed = !this.collapsed;\n this.dispatchEvent(new CustomEvent(\"collapse-clicked\", {}));\n }\n renderIcon() {\n return ke`\n ${this.icon}\n `;\n }\n renderCollapseButton() {\n let icon;\n if (this.compactMode) {\n icon = this.renderIcon();\n } else if (this.collapsed) {\n icon = ke``;\n } else {\n icon = ke``;\n }\n return ke`\n ${icon}\n `;\n }\n};\n__decorateClass([\n n4({ type: String })\n], Container.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], Container.prototype, \"title\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], Container.prototype, \"collapsed\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], Container.prototype, \"hideCloseButton\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], Container.prototype, \"compactMode\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], Container.prototype, \"noHeader\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], Container.prototype, \"reverseHeader\", 2);\nif (!customElements.get(Container.componentName)) {\n customElements.define(Container.componentName, Container);\n}\n\n// node_modules/lit-html/directives/style-map.js\nvar ee = \"important\";\nvar ie = \" !\" + ee;\nvar se = e5(class extends i3 {\n constructor(e6) {\n if (super(e6), e6.type !== t2.ATTRIBUTE || \"style\" !== e6.name || e6.strings?.length > 2) throw Error(\"The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return Object.keys(t3).reduce((e6, r5) => {\n const s2 = t3[r5];\n return null == s2 ? e6 : e6 + `${r5 = r5.includes(\"-\") ? r5 : r5.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g, \"-$&\").toLowerCase()}:${s2};`;\n }, \"\");\n }\n update(t3, [e6]) {\n const { style: r5 } = t3.element;\n if (void 0 === this.ft) return this.ft = new Set(Object.keys(e6)), this.render(e6);\n for (const t4 of this.ft) null == e6[t4] && (this.ft.delete(t4), t4.includes(\"-\") ? r5.removeProperty(t4) : r5[t4] = null);\n for (const t4 in e6) {\n const s2 = e6[t4];\n if (null != s2) {\n this.ft.add(t4);\n const e7 = \"string\" == typeof s2 && s2.endsWith(ie);\n t4.includes(\"-\") || e7 ? r5.setProperty(t4, e7 ? s2.slice(0, -11) : s2, e7 ? ee : \"\") : r5[t4] = s2;\n }\n }\n return R;\n }\n});\n\n// js/tab_panel.ts\nfunction convertToId(name) {\n return (name || \"\").trim().replace(\" \", \"-\").toLowerCase();\n}\nvar TabPanel = class extends h3 {\n constructor() {\n super(...arguments);\n this.tabs = [];\n this.index = 0;\n this.mode = 1 /* HIDE_ON_SECOND_CLICK */;\n this.alignment = \"left\" /* LEFT */;\n }\n static get componentName() {\n return `tab-panel`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n ::slotted(*) {\n display: none;\n }\n\n ::slotted(.show-tab) {\n display: block;\n }\n\n .container {\n padding: 0;\n width: 100%;\n }\n\n .tab-container {\n align-items: center;\n display: flex;\n flex-direction: row;\n }\n\n .tab-container.center {\n justify-content: center;\n }\n\n .tab-container.left {\n justify-content: flex-start;\n }\n\n .tab-container.right {\n justify-content: flex-end;\n }\n\n .tab-container button {\n border-radius: 5px;\n height: 28px;\n margin: 2px 0 2px 8px;\n user-select: none;\n }\n\n .tab-container button.icon {\n font-size: 16px;\n width: 28px;\n }\n\n .tab-container button.name {\n padding: 0 8px;\n }\n `\n ];\n }\n render() {\n return ke`\n
    \n \n ${this.renderTabs()}\n
    \n \n \n `;\n }\n update(changedProperties) {\n super.update(changedProperties);\n if (changedProperties.has(\"index\") && changedProperties.get(\"index\") != null) {\n this.updateSlotChildren();\n }\n }\n updateSlotChildren() {\n if (!this.tabContentElements) {\n return;\n }\n this.tabContentElements.forEach((element, i4) => {\n element.classList.remove(\"show-tab\");\n const id = convertToId(this.tabs[i4].name);\n element.setAttribute(\"id\", `tabpanel-${id}-${i4}`);\n element.setAttribute(\"role\", \"tabpanel\");\n element.setAttribute(\"aria-labelledby\", `tab-${id}-${i4}`);\n });\n this.tabContentElements[this.index]?.classList.add(\"show-tab\");\n }\n renderTabs() {\n return this.tabs.map((tab, i4) => {\n const id = convertToId(this.tabs[i4].name);\n return ke` {\n this.onTabClick(i4);\n }}>\n ${tab.icon ? ke`${tab.icon}` : D}\n ${tab.name}\n `;\n });\n }\n onTabClick(index) {\n switch (this.mode) {\n case 1 /* HIDE_ON_SECOND_CLICK */:\n this.index = this.index === index ? -1 : index;\n break;\n case 0 /* ALWAYS_SHOW */:\n default:\n this.index = index;\n }\n this.dispatchEvent(new CustomEvent(\"tab-changed\", {\n detail: index\n }));\n }\n};\n__decorateClass([\n n4({ type: Array })\n], TabPanel.prototype, \"tabs\", 2);\n__decorateClass([\n n4({ type: Number })\n], TabPanel.prototype, \"index\", 2);\n__decorateClass([\n n4({ type: Number })\n], TabPanel.prototype, \"mode\", 2);\n__decorateClass([\n n4({ type: String })\n], TabPanel.prototype, \"alignment\", 2);\n__decorateClass([\n r4(\".tab\")\n], TabPanel.prototype, \"tabElements\", 2);\n__decorateClass([\n o4()\n], TabPanel.prototype, \"tabContentElements\", 2);\nif (!customElements.get(TabPanel.componentName)) {\n customElements.define(TabPanel.componentName, TabPanel);\n}\n\n// js/toolbar.ts\nvar Toolbar = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.expanded = false;\n }\n static get componentName() {\n return `toolbar-panel`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n .hide {\n display: none;\n }\n\n .expanded {\n display: block; !important\n }\n\n slot[name=\"extra-tools\"] {\n margin-top: 4px;\n }\n\n ::slotted([slot=\"main-tools\"]),\n ::slotted([slot=\"extra-tools\"]) {\n align-items: center;\n display: inline-grid;\n grid-template-columns: auto auto auto;\n grid-gap: 4px;\n justify-items: center;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"main_tools\", null],\n [\"extra_tools\", null],\n [\"expanded\", \"expanded\"]\n ]);\n }\n render() {\n return ke`\n \n
    \n \n \n
    \n \n `;\n }\n};\n__decorateClass([\n n4()\n], Toolbar.prototype, \"expanded\", 2);\nif (!customElements.get(Toolbar.componentName)) {\n customElements.define(Toolbar.componentName, Toolbar);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(Toolbar.componentName);\n manager.model = model;\n el.appendChild(manager);\n const mainToolsEl = document.createElement(\"div\");\n mainToolsEl.slot = \"main-tools\";\n manager.appendChild(mainToolsEl);\n updateChildren(mainToolsEl, model, \"main_tools\");\n model.on(\"change:main_tools\", () => {\n updateChildren(mainToolsEl, model, \"main_tools\");\n });\n const extraToolsEl = document.createElement(\"div\");\n extraToolsEl.slot = \"extra-tools\";\n manager.appendChild(extraToolsEl);\n updateChildren(extraToolsEl, model, \"extra_tools\");\n model.on(\"change:extra_tools\", () => {\n updateChildren(extraToolsEl, model, \"extra_tools\");\n });\n}\nvar toolbar_default = { render };\nexport {\n Toolbar,\n toolbar_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/style-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "expanded": false, + "extra_tools": [ + "IPY_MODEL_9b1e99aba6cb4b7782d78e56ea005d9d", + "IPY_MODEL_c6576ff54bff4e7a9fb410443d3ecc5b", + "IPY_MODEL_4e5bda9157e7494bafcaf8e44f864424", + "IPY_MODEL_e6c172ac8c3f4e4ca08400f839230174", + "IPY_MODEL_b97ac9c1ea2e46e4a0bd07ab88ae012c", + "IPY_MODEL_262c8654c40643898621dfb02718aeb4", + "IPY_MODEL_6907b83887184f18a9811508ec9887f9", + "IPY_MODEL_db63bba9f9c64f4787c328766671edcb", + "IPY_MODEL_5c89fe5b921d45bbbe5594552c80ddd6", + "IPY_MODEL_ac2cd52fca484632832a8ffc460f2e98", + "IPY_MODEL_9b1aa26a079f4cdb933d90202e373ea5" + ], + "layout": "IPY_MODEL_50efdb138316463cbdb7fcafc36e4f0d", + "main_tools": [ + "IPY_MODEL_320be3525f7244e59fa1e32a4cd9bcf3", + "IPY_MODEL_018bc35395594b90912c927e2211212e", + "IPY_MODEL_1e753e5cf2ff4b53ac41e818e9f7bf7b", + "IPY_MODEL_d110ce8e35f140d585e81e88cce4a413", + "IPY_MODEL_060518aa3c224037930316d880e07fc8", + "IPY_MODEL_b9aa9ab9f4044505bdf204d794b88df3" + ] + } + }, + "08df80e9b6e94b60a1f7b30a1e96b6b8": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": "0px 10px", + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": "auto auto", + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": "visible", + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "0164447f09534c85a9bc9ba3b0a8ced5": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "a0a9213c4e3e464287da4bb6517408e5": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "50efdb138316463cbdb7fcafc36e4f0d": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d6061d8c11874c50a4379be53a5e3029": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.map_widgets.LayerManagerRow", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/layer_manager_row.ts\nvar LayerManagerRow = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.name = \"\";\n this.visible = true;\n this.opacity = 1;\n this.isLoading = false;\n this.isConfirmDialogVisible = false;\n }\n static get componentName() {\n return `layer-manager-row`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n .row {\n align-items: center;\n display: flex;\n gap: 4px;\n }\n\n .layer-name {\n cursor: pointer;\n flex-grow: 1;\n max-width: 150px;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .row-button {\n font-size: 16px;\n height: 28px;\n width: 28px;\n }\n\n .layer-opacity-slider {\n width: 70px;\n }\n\n .layer-visibility-checkbox {\n margin: 2px;\n }\n\n .spinner {\n -webkit-animation: spin 2s linear infinite;\n animation: spin 2s linear infinite;\n border-radius: 50%;\n border: 4px solid var(--jp-widgets-input-border-color);\n border-top: 4px solid var(--jp-widgets-color);\n height: 12px;\n width: 12px;\n }\n\n @-webkit-keyframes spin {\n 0% {\n -webkit-transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(360deg);\n }\n }\n\n @keyframes spin {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n }\n\n button.loading .spinner,\n button.loading:hover .close-icon,\n button.done-loading .close-icon {\n display: block;\n }\n\n button.loading .close-icon,\n button.loading:hover .spinner,\n button.done-loading .spinner {\n display: none;\n }\n\n .remove-layer-text {\n flex-grow: 1;\n padding-left: 22px;\n }\n\n .confirm-deletion-container {\n margin-top: 4px;\n }\n\n .confirm-deletion-container button {\n height: 28px;\n width: 70px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"name\", \"name\"],\n [\"visible\", \"visible\"],\n [\"opacity\", \"opacity\"],\n [\"is_loading\", \"isLoading\"]\n ]);\n }\n render() {\n return ke`\n
    \n \n \n ${this.name}\n \n \n \n settings\n \n \n
    \n delete
    \n \n
    \n ${this.renderConfirmDialog()}\n `;\n }\n renderConfirmDialog() {\n if (!this.isConfirmDialogVisible) {\n return D;\n }\n return ke`\n
    \n Remove layer?\n \n No\n \n \n Yes\n \n
    \n `;\n }\n onLayerVisibilityChanged(_event) {\n this.visible = !this.visible;\n }\n onLayerOpacityChanged(event) {\n const target = event.target;\n this.opacity = parseFloat(target.value);\n }\n onSettingsClicked(_2) {\n this.model?.send({ type: \"click\", id: \"settings\" });\n }\n onDeleteClicked(_2) {\n this.isConfirmDialogVisible = true;\n }\n confirmDeletion(_2) {\n this.model?.send({ type: \"click\", id: \"delete\" });\n }\n cancelDeletion(_2) {\n this.isConfirmDialogVisible = false;\n }\n};\n__decorateClass([\n n4()\n], LayerManagerRow.prototype, \"name\", 2);\n__decorateClass([\n n4()\n], LayerManagerRow.prototype, \"visible\", 2);\n__decorateClass([\n n4()\n], LayerManagerRow.prototype, \"opacity\", 2);\n__decorateClass([\n n4()\n], LayerManagerRow.prototype, \"isLoading\", 2);\n__decorateClass([\n n4()\n], LayerManagerRow.prototype, \"isConfirmDialogVisible\", 2);\nif (!customElements.get(LayerManagerRow.componentName)) {\n customElements.define(LayerManagerRow.componentName, LayerManagerRow);\n}\nfunction render({ model, el }) {\n loadFonts();\n const row = document.createElement(LayerManagerRow.componentName);\n row.model = model;\n el.appendChild(row);\n}\nvar layer_manager_row_default = { render };\nexport {\n LayerManagerRow,\n layer_manager_row_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "is_loading": false, + "layout": "IPY_MODEL_7dc38477ca5145948632181e660a72ca", + "name": "Esri.WorldImagery", + "opacity": 1, + "visible": true + } + }, + "0f756d3b935c4b14a4aca5d85723c84d": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.map_widgets.LayerManagerRow", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/layer_manager_row.ts\nvar LayerManagerRow = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.name = \"\";\n this.visible = true;\n this.opacity = 1;\n this.isLoading = false;\n this.isConfirmDialogVisible = false;\n }\n static get componentName() {\n return `layer-manager-row`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n .row {\n align-items: center;\n display: flex;\n gap: 4px;\n }\n\n .layer-name {\n cursor: pointer;\n flex-grow: 1;\n max-width: 150px;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .row-button {\n font-size: 16px;\n height: 28px;\n width: 28px;\n }\n\n .layer-opacity-slider {\n width: 70px;\n }\n\n .layer-visibility-checkbox {\n margin: 2px;\n }\n\n .spinner {\n -webkit-animation: spin 2s linear infinite;\n animation: spin 2s linear infinite;\n border-radius: 50%;\n border: 4px solid var(--jp-widgets-input-border-color);\n border-top: 4px solid var(--jp-widgets-color);\n height: 12px;\n width: 12px;\n }\n\n @-webkit-keyframes spin {\n 0% {\n -webkit-transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(360deg);\n }\n }\n\n @keyframes spin {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n }\n\n button.loading .spinner,\n button.loading:hover .close-icon,\n button.done-loading .close-icon {\n display: block;\n }\n\n button.loading .close-icon,\n button.loading:hover .spinner,\n button.done-loading .spinner {\n display: none;\n }\n\n .remove-layer-text {\n flex-grow: 1;\n padding-left: 22px;\n }\n\n .confirm-deletion-container {\n margin-top: 4px;\n }\n\n .confirm-deletion-container button {\n height: 28px;\n width: 70px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"name\", \"name\"],\n [\"visible\", \"visible\"],\n [\"opacity\", \"opacity\"],\n [\"is_loading\", \"isLoading\"]\n ]);\n }\n render() {\n return ke`\n
    \n \n \n ${this.name}\n \n \n \n settings\n \n \n
    \n delete
    \n \n
    \n ${this.renderConfirmDialog()}\n `;\n }\n renderConfirmDialog() {\n if (!this.isConfirmDialogVisible) {\n return D;\n }\n return ke`\n
    \n Remove layer?\n \n No\n \n \n Yes\n \n
    \n `;\n }\n onLayerVisibilityChanged(_event) {\n this.visible = !this.visible;\n }\n onLayerOpacityChanged(event) {\n const target = event.target;\n this.opacity = parseFloat(target.value);\n }\n onSettingsClicked(_2) {\n this.model?.send({ type: \"click\", id: \"settings\" });\n }\n onDeleteClicked(_2) {\n this.isConfirmDialogVisible = true;\n }\n confirmDeletion(_2) {\n this.model?.send({ type: \"click\", id: \"delete\" });\n }\n cancelDeletion(_2) {\n this.isConfirmDialogVisible = false;\n }\n};\n__decorateClass([\n n4()\n], LayerManagerRow.prototype, \"name\", 2);\n__decorateClass([\n n4()\n], LayerManagerRow.prototype, \"visible\", 2);\n__decorateClass([\n n4()\n], LayerManagerRow.prototype, \"opacity\", 2);\n__decorateClass([\n n4()\n], LayerManagerRow.prototype, \"isLoading\", 2);\n__decorateClass([\n n4()\n], LayerManagerRow.prototype, \"isConfirmDialogVisible\", 2);\nif (!customElements.get(LayerManagerRow.componentName)) {\n customElements.define(LayerManagerRow.componentName, LayerManagerRow);\n}\nfunction render({ model, el }) {\n loadFonts();\n const row = document.createElement(LayerManagerRow.componentName);\n row.model = model;\n el.appendChild(row);\n}\nvar layer_manager_row_default = { render };\nexport {\n LayerManagerRow,\n layer_manager_row_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "is_loading": false, + "layout": "IPY_MODEL_3117c8f8dc454974ad725820bbaff1b6", + "name": "25m DW Tree Cover", + "opacity": 1, + "visible": true + } + }, + "d25e876b06b84a57a476a14ce924b60a": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.map_widgets.LayerManagerRow", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/layer_manager_row.ts\nvar LayerManagerRow = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.name = \"\";\n this.visible = true;\n this.opacity = 1;\n this.isLoading = false;\n this.isConfirmDialogVisible = false;\n }\n static get componentName() {\n return `layer-manager-row`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n .row {\n align-items: center;\n display: flex;\n gap: 4px;\n }\n\n .layer-name {\n cursor: pointer;\n flex-grow: 1;\n max-width: 150px;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .row-button {\n font-size: 16px;\n height: 28px;\n width: 28px;\n }\n\n .layer-opacity-slider {\n width: 70px;\n }\n\n .layer-visibility-checkbox {\n margin: 2px;\n }\n\n .spinner {\n -webkit-animation: spin 2s linear infinite;\n animation: spin 2s linear infinite;\n border-radius: 50%;\n border: 4px solid var(--jp-widgets-input-border-color);\n border-top: 4px solid var(--jp-widgets-color);\n height: 12px;\n width: 12px;\n }\n\n @-webkit-keyframes spin {\n 0% {\n -webkit-transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(360deg);\n }\n }\n\n @keyframes spin {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n }\n\n button.loading .spinner,\n button.loading:hover .close-icon,\n button.done-loading .close-icon {\n display: block;\n }\n\n button.loading .close-icon,\n button.loading:hover .spinner,\n button.done-loading .spinner {\n display: none;\n }\n\n .remove-layer-text {\n flex-grow: 1;\n padding-left: 22px;\n }\n\n .confirm-deletion-container {\n margin-top: 4px;\n }\n\n .confirm-deletion-container button {\n height: 28px;\n width: 70px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"name\", \"name\"],\n [\"visible\", \"visible\"],\n [\"opacity\", \"opacity\"],\n [\"is_loading\", \"isLoading\"]\n ]);\n }\n render() {\n return ke`\n
    \n \n \n ${this.name}\n \n \n \n settings\n \n \n
    \n delete
    \n \n
    \n ${this.renderConfirmDialog()}\n `;\n }\n renderConfirmDialog() {\n if (!this.isConfirmDialogVisible) {\n return D;\n }\n return ke`\n
    \n Remove layer?\n \n No\n \n \n Yes\n \n
    \n `;\n }\n onLayerVisibilityChanged(_event) {\n this.visible = !this.visible;\n }\n onLayerOpacityChanged(event) {\n const target = event.target;\n this.opacity = parseFloat(target.value);\n }\n onSettingsClicked(_2) {\n this.model?.send({ type: \"click\", id: \"settings\" });\n }\n onDeleteClicked(_2) {\n this.isConfirmDialogVisible = true;\n }\n confirmDeletion(_2) {\n this.model?.send({ type: \"click\", id: \"delete\" });\n }\n cancelDeletion(_2) {\n this.isConfirmDialogVisible = false;\n }\n};\n__decorateClass([\n n4()\n], LayerManagerRow.prototype, \"name\", 2);\n__decorateClass([\n n4()\n], LayerManagerRow.prototype, \"visible\", 2);\n__decorateClass([\n n4()\n], LayerManagerRow.prototype, \"opacity\", 2);\n__decorateClass([\n n4()\n], LayerManagerRow.prototype, \"isLoading\", 2);\n__decorateClass([\n n4()\n], LayerManagerRow.prototype, \"isConfirmDialogVisible\", 2);\nif (!customElements.get(LayerManagerRow.componentName)) {\n customElements.define(LayerManagerRow.componentName, LayerManagerRow);\n}\nfunction render({ model, el }) {\n loadFonts();\n const row = document.createElement(LayerManagerRow.componentName);\n row.model = model;\n el.appendChild(row);\n}\nvar layer_manager_row_default = { render };\nexport {\n LayerManagerRow,\n layer_manager_row_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "is_loading": false, + "layout": "IPY_MODEL_f306390cbeee4c17acad2a7faea2bda7", + "name": "LTP-STP Image", + "opacity": 1, + "visible": true + } + }, + "320be3525f7244e59fa1e32a4cd9bcf3": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.ToolbarItem", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/toolbar_item.ts\nvar ToolbarItem = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.active = false;\n this.primary = true;\n this.icon = \"\";\n this.tooltip_text = \"\";\n }\n static get componentName() {\n return `tool-button`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n button {\n font-size: 16px !important;\n height: 32px;\n padding: 0px 0px 0px 4px;\n width: 32px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"active\", \"active\"],\n [\"primary\", \"primary\"],\n [\"icon\", \"icon\"],\n [\"tooltip_text\", \"tooltip_text\"]\n ]);\n }\n render() {\n return ke`\n \n ${this.icon}\n `;\n }\n onClick(_2) {\n this.active = !this.active;\n }\n};\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"active\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"primary\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"tooltip_text\", 2);\nif (!customElements.get(ToolbarItem.componentName)) {\n customElements.define(ToolbarItem.componentName, ToolbarItem);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(ToolbarItem.componentName);\n manager.model = model;\n el.appendChild(manager);\n}\nvar toolbar_item_default = { render };\nexport {\n ToolbarItem,\n toolbar_item_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "active": false, + "icon": "point_scan", + "layout": "IPY_MODEL_bd44dc656406487e96cbe194c6261bd0", + "primary": true, + "tooltip_text": "Inspector" + } + }, + "018bc35395594b90912c927e2211212e": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.ToolbarItem", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/toolbar_item.ts\nvar ToolbarItem = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.active = false;\n this.primary = true;\n this.icon = \"\";\n this.tooltip_text = \"\";\n }\n static get componentName() {\n return `tool-button`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n button {\n font-size: 16px !important;\n height: 32px;\n padding: 0px 0px 0px 4px;\n width: 32px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"active\", \"active\"],\n [\"primary\", \"primary\"],\n [\"icon\", \"icon\"],\n [\"tooltip_text\", \"tooltip_text\"]\n ]);\n }\n render() {\n return ke`\n \n ${this.icon}\n `;\n }\n onClick(_2) {\n this.active = !this.active;\n }\n};\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"active\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"primary\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"tooltip_text\", 2);\nif (!customElements.get(ToolbarItem.componentName)) {\n customElements.define(ToolbarItem.componentName, ToolbarItem);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(ToolbarItem.componentName);\n manager.model = model;\n el.appendChild(manager);\n}\nvar toolbar_item_default = { render };\nexport {\n ToolbarItem,\n toolbar_item_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "active": false, + "icon": "bar_chart", + "layout": "IPY_MODEL_e9f6bd2414c84f258c433913fe8cf03e", + "primary": true, + "tooltip_text": "Plotting" + } + }, + "1e753e5cf2ff4b53ac41e818e9f7bf7b": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.ToolbarItem", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/toolbar_item.ts\nvar ToolbarItem = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.active = false;\n this.primary = true;\n this.icon = \"\";\n this.tooltip_text = \"\";\n }\n static get componentName() {\n return `tool-button`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n button {\n font-size: 16px !important;\n height: 32px;\n padding: 0px 0px 0px 4px;\n width: 32px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"active\", \"active\"],\n [\"primary\", \"primary\"],\n [\"icon\", \"icon\"],\n [\"tooltip_text\", \"tooltip_text\"]\n ]);\n }\n render() {\n return ke`\n \n ${this.icon}\n `;\n }\n onClick(_2) {\n this.active = !this.active;\n }\n};\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"active\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"primary\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"tooltip_text\", 2);\nif (!customElements.get(ToolbarItem.componentName)) {\n customElements.define(ToolbarItem.componentName, ToolbarItem);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(ToolbarItem.componentName);\n manager.model = model;\n el.appendChild(manager);\n}\nvar toolbar_item_default = { render };\nexport {\n ToolbarItem,\n toolbar_item_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "active": false, + "icon": "history", + "layout": "IPY_MODEL_f0268b4fa5f8426aab5467c9b24a5fdd", + "primary": true, + "tooltip_text": "Create timelapse" + } + }, + "d110ce8e35f140d585e81e88cce4a413": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.ToolbarItem", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/toolbar_item.ts\nvar ToolbarItem = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.active = false;\n this.primary = true;\n this.icon = \"\";\n this.tooltip_text = \"\";\n }\n static get componentName() {\n return `tool-button`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n button {\n font-size: 16px !important;\n height: 32px;\n padding: 0px 0px 0px 4px;\n width: 32px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"active\", \"active\"],\n [\"primary\", \"primary\"],\n [\"icon\", \"icon\"],\n [\"tooltip_text\", \"tooltip_text\"]\n ]);\n }\n render() {\n return ke`\n \n ${this.icon}\n `;\n }\n onClick(_2) {\n this.active = !this.active;\n }\n};\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"active\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"primary\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"tooltip_text\", 2);\nif (!customElements.get(ToolbarItem.componentName)) {\n customElements.define(ToolbarItem.componentName, ToolbarItem);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(ToolbarItem.componentName);\n manager.model = model;\n el.appendChild(manager);\n}\nvar toolbar_item_default = { render };\nexport {\n ToolbarItem,\n toolbar_item_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "active": false, + "icon": "map", + "layout": "IPY_MODEL_c5f72af9d368407dbaf5d9e4d7b91392", + "primary": true, + "tooltip_text": "Change basemap" + } + }, + "060518aa3c224037930316d880e07fc8": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.ToolbarItem", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/toolbar_item.ts\nvar ToolbarItem = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.active = false;\n this.primary = true;\n this.icon = \"\";\n this.tooltip_text = \"\";\n }\n static get componentName() {\n return `tool-button`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n button {\n font-size: 16px !important;\n height: 32px;\n padding: 0px 0px 0px 4px;\n width: 32px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"active\", \"active\"],\n [\"primary\", \"primary\"],\n [\"icon\", \"icon\"],\n [\"tooltip_text\", \"tooltip_text\"]\n ]);\n }\n render() {\n return ke`\n \n ${this.icon}\n `;\n }\n onClick(_2) {\n this.active = !this.active;\n }\n};\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"active\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"primary\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"tooltip_text\", 2);\nif (!customElements.get(ToolbarItem.componentName)) {\n customElements.define(ToolbarItem.componentName, ToolbarItem);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(ToolbarItem.componentName);\n manager.model = model;\n el.appendChild(manager);\n}\nvar toolbar_item_default = { render };\nexport {\n ToolbarItem,\n toolbar_item_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "active": false, + "icon": "code", + "layout": "IPY_MODEL_91e3f52fd7364d319305e5af41f1c82c", + "primary": true, + "tooltip_text": "Convert Earth Engine JavaScript to Python" + } + }, + "b9aa9ab9f4044505bdf204d794b88df3": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.ToolbarItem", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/toolbar_item.ts\nvar ToolbarItem = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.active = false;\n this.primary = true;\n this.icon = \"\";\n this.tooltip_text = \"\";\n }\n static get componentName() {\n return `tool-button`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n button {\n font-size: 16px !important;\n height: 32px;\n padding: 0px 0px 0px 4px;\n width: 32px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"active\", \"active\"],\n [\"primary\", \"primary\"],\n [\"icon\", \"icon\"],\n [\"tooltip_text\", \"tooltip_text\"]\n ]);\n }\n render() {\n return ke`\n \n ${this.icon}\n `;\n }\n onClick(_2) {\n this.active = !this.active;\n }\n};\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"active\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"primary\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"tooltip_text\", 2);\nif (!customElements.get(ToolbarItem.componentName)) {\n customElements.define(ToolbarItem.componentName, ToolbarItem);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(ToolbarItem.componentName);\n manager.model = model;\n el.appendChild(manager);\n}\nvar toolbar_item_default = { render };\nexport {\n ToolbarItem,\n toolbar_item_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "active": false, + "icon": "add", + "layout": "IPY_MODEL_125e8e3b0ac04f6993b13c51a0380138", + "primary": true, + "tooltip_text": "Expand toolbar" + } + }, + "9b1e99aba6cb4b7782d78e56ea005d9d": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.ToolbarItem", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/toolbar_item.ts\nvar ToolbarItem = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.active = false;\n this.primary = true;\n this.icon = \"\";\n this.tooltip_text = \"\";\n }\n static get componentName() {\n return `tool-button`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n button {\n font-size: 16px !important;\n height: 32px;\n padding: 0px 0px 0px 4px;\n width: 32px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"active\", \"active\"],\n [\"primary\", \"primary\"],\n [\"icon\", \"icon\"],\n [\"tooltip_text\", \"tooltip_text\"]\n ]);\n }\n render() {\n return ke`\n \n ${this.icon}\n `;\n }\n onClick(_2) {\n this.active = !this.active;\n }\n};\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"active\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"primary\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"tooltip_text\", 2);\nif (!customElements.get(ToolbarItem.componentName)) {\n customElements.define(ToolbarItem.componentName, ToolbarItem);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(ToolbarItem.componentName);\n manager.model = model;\n el.appendChild(manager);\n}\nvar toolbar_item_default = { render };\nexport {\n ToolbarItem,\n toolbar_item_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "active": false, + "icon": "ink_eraser", + "layout": "IPY_MODEL_ea343d9e0bb749998ee948e3b48e087f", + "primary": true, + "tooltip_text": "Remove all drawn features" + } + }, + "c6576ff54bff4e7a9fb410443d3ecc5b": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.ToolbarItem", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/toolbar_item.ts\nvar ToolbarItem = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.active = false;\n this.primary = true;\n this.icon = \"\";\n this.tooltip_text = \"\";\n }\n static get componentName() {\n return `tool-button`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n button {\n font-size: 16px !important;\n height: 32px;\n padding: 0px 0px 0px 4px;\n width: 32px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"active\", \"active\"],\n [\"primary\", \"primary\"],\n [\"icon\", \"icon\"],\n [\"tooltip_text\", \"tooltip_text\"]\n ]);\n }\n render() {\n return ke`\n \n ${this.icon}\n `;\n }\n onClick(_2) {\n this.active = !this.active;\n }\n};\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"active\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"primary\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"tooltip_text\", 2);\nif (!customElements.get(ToolbarItem.componentName)) {\n customElements.define(ToolbarItem.componentName, ToolbarItem);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(ToolbarItem.componentName);\n manager.model = model;\n el.appendChild(manager);\n}\nvar toolbar_item_default = { render };\nexport {\n ToolbarItem,\n toolbar_item_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "active": false, + "icon": "upload", + "layout": "IPY_MODEL_8038eb785016462b8c9b593921119002", + "primary": true, + "tooltip_text": "Open local vector/raster data" + } + }, + "4e5bda9157e7494bafcaf8e44f864424": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.ToolbarItem", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/toolbar_item.ts\nvar ToolbarItem = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.active = false;\n this.primary = true;\n this.icon = \"\";\n this.tooltip_text = \"\";\n }\n static get componentName() {\n return `tool-button`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n button {\n font-size: 16px !important;\n height: 32px;\n padding: 0px 0px 0px 4px;\n width: 32px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"active\", \"active\"],\n [\"primary\", \"primary\"],\n [\"icon\", \"icon\"],\n [\"tooltip_text\", \"tooltip_text\"]\n ]);\n }\n render() {\n return ke`\n \n ${this.icon}\n `;\n }\n onClick(_2) {\n this.active = !this.active;\n }\n};\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"active\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"primary\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"tooltip_text\", 2);\nif (!customElements.get(ToolbarItem.componentName)) {\n customElements.define(ToolbarItem.componentName, ToolbarItem);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(ToolbarItem.componentName);\n manager.model = model;\n el.appendChild(manager);\n}\nvar toolbar_item_default = { render };\nexport {\n ToolbarItem,\n toolbar_item_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "active": false, + "icon": "manufacturing", + "layout": "IPY_MODEL_fbb8c80ca0784cef9d6139a5fbbb6b46", + "primary": true, + "tooltip_text": "WhiteboxTools for local geoprocessing" + } + }, + "e6c172ac8c3f4e4ca08400f839230174": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.ToolbarItem", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/toolbar_item.ts\nvar ToolbarItem = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.active = false;\n this.primary = true;\n this.icon = \"\";\n this.tooltip_text = \"\";\n }\n static get componentName() {\n return `tool-button`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n button {\n font-size: 16px !important;\n height: 32px;\n padding: 0px 0px 0px 4px;\n width: 32px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"active\", \"active\"],\n [\"primary\", \"primary\"],\n [\"icon\", \"icon\"],\n [\"tooltip_text\", \"tooltip_text\"]\n ]);\n }\n render() {\n return ke`\n \n ${this.icon}\n `;\n }\n onClick(_2) {\n this.active = !this.active;\n }\n};\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"active\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"primary\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"tooltip_text\", 2);\nif (!customElements.get(ToolbarItem.componentName)) {\n customElements.define(ToolbarItem.componentName, ToolbarItem);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(ToolbarItem.componentName);\n manager.model = model;\n el.appendChild(manager);\n}\nvar toolbar_item_default = { render };\nexport {\n ToolbarItem,\n toolbar_item_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "active": false, + "icon": "dns", + "layout": "IPY_MODEL_49cfc07507794d6aab0419f0f0f17ef4", + "primary": true, + "tooltip_text": "GEE Toolbox for cloud computing" + } + }, + "b97ac9c1ea2e46e4a0bd07ab88ae012c": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.ToolbarItem", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/toolbar_item.ts\nvar ToolbarItem = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.active = false;\n this.primary = true;\n this.icon = \"\";\n this.tooltip_text = \"\";\n }\n static get componentName() {\n return `tool-button`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n button {\n font-size: 16px !important;\n height: 32px;\n padding: 0px 0px 0px 4px;\n width: 32px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"active\", \"active\"],\n [\"primary\", \"primary\"],\n [\"icon\", \"icon\"],\n [\"tooltip_text\", \"tooltip_text\"]\n ]);\n }\n render() {\n return ke`\n \n ${this.icon}\n `;\n }\n onClick(_2) {\n this.active = !this.active;\n }\n};\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"active\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"primary\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"tooltip_text\", 2);\nif (!customElements.get(ToolbarItem.componentName)) {\n customElements.define(ToolbarItem.componentName, ToolbarItem);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(ToolbarItem.componentName);\n manager.model = model;\n el.appendChild(manager);\n}\nvar toolbar_item_default = { render };\nexport {\n ToolbarItem,\n toolbar_item_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "active": false, + "icon": "fast_forward", + "layout": "IPY_MODEL_44d6efda35304e3b82becaa0554cc00a", + "primary": true, + "tooltip_text": "Activate timeslider" + } + }, + "262c8654c40643898621dfb02718aeb4": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.ToolbarItem", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/toolbar_item.ts\nvar ToolbarItem = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.active = false;\n this.primary = true;\n this.icon = \"\";\n this.tooltip_text = \"\";\n }\n static get componentName() {\n return `tool-button`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n button {\n font-size: 16px !important;\n height: 32px;\n padding: 0px 0px 0px 4px;\n width: 32px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"active\", \"active\"],\n [\"primary\", \"primary\"],\n [\"icon\", \"icon\"],\n [\"tooltip_text\", \"tooltip_text\"]\n ]);\n }\n render() {\n return ke`\n \n ${this.icon}\n `;\n }\n onClick(_2) {\n this.active = !this.active;\n }\n};\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"active\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"primary\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"tooltip_text\", 2);\nif (!customElements.get(ToolbarItem.componentName)) {\n customElements.define(ToolbarItem.componentName, ToolbarItem);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(ToolbarItem.componentName);\n manager.model = model;\n el.appendChild(manager);\n}\nvar toolbar_item_default = { render };\nexport {\n ToolbarItem,\n toolbar_item_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "active": false, + "icon": "pan_tool_alt", + "layout": "IPY_MODEL_a386181ab2a948f39200afa36498a19e", + "primary": true, + "tooltip_text": "Collect training samples" + } + }, + "6907b83887184f18a9811508ec9887f9": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.ToolbarItem", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/toolbar_item.ts\nvar ToolbarItem = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.active = false;\n this.primary = true;\n this.icon = \"\";\n this.tooltip_text = \"\";\n }\n static get componentName() {\n return `tool-button`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n button {\n font-size: 16px !important;\n height: 32px;\n padding: 0px 0px 0px 4px;\n width: 32px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"active\", \"active\"],\n [\"primary\", \"primary\"],\n [\"icon\", \"icon\"],\n [\"tooltip_text\", \"tooltip_text\"]\n ]);\n }\n render() {\n return ke`\n \n ${this.icon}\n `;\n }\n onClick(_2) {\n this.active = !this.active;\n }\n};\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"active\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"primary\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"tooltip_text\", 2);\nif (!customElements.get(ToolbarItem.componentName)) {\n customElements.define(ToolbarItem.componentName, ToolbarItem);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(ToolbarItem.componentName);\n manager.model = model;\n el.appendChild(manager);\n}\nvar toolbar_item_default = { render };\nexport {\n ToolbarItem,\n toolbar_item_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "active": false, + "icon": "show_chart", + "layout": "IPY_MODEL_313ce92ad01147d1bd2f3e3491f0fabf", + "primary": true, + "tooltip_text": "Creating and plotting transects" + } + }, + "db63bba9f9c64f4787c328766671edcb": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.ToolbarItem", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/toolbar_item.ts\nvar ToolbarItem = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.active = false;\n this.primary = true;\n this.icon = \"\";\n this.tooltip_text = \"\";\n }\n static get componentName() {\n return `tool-button`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n button {\n font-size: 16px !important;\n height: 32px;\n padding: 0px 0px 0px 4px;\n width: 32px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"active\", \"active\"],\n [\"primary\", \"primary\"],\n [\"icon\", \"icon\"],\n [\"tooltip_text\", \"tooltip_text\"]\n ]);\n }\n render() {\n return ke`\n \n ${this.icon}\n `;\n }\n onClick(_2) {\n this.active = !this.active;\n }\n};\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"active\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"primary\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"tooltip_text\", 2);\nif (!customElements.get(ToolbarItem.componentName)) {\n customElements.define(ToolbarItem.componentName, ToolbarItem);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(ToolbarItem.componentName);\n manager.model = model;\n el.appendChild(manager);\n}\nvar toolbar_item_default = { render };\nexport {\n ToolbarItem,\n toolbar_item_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "active": false, + "icon": "shuffle", + "layout": "IPY_MODEL_b6f2cd884b4140c2b954178b4a3f5ffb", + "primary": true, + "tooltip_text": "Sankey plots" + } + }, + "5c89fe5b921d45bbbe5594552c80ddd6": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.ToolbarItem", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/toolbar_item.ts\nvar ToolbarItem = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.active = false;\n this.primary = true;\n this.icon = \"\";\n this.tooltip_text = \"\";\n }\n static get componentName() {\n return `tool-button`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n button {\n font-size: 16px !important;\n height: 32px;\n padding: 0px 0px 0px 4px;\n width: 32px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"active\", \"active\"],\n [\"primary\", \"primary\"],\n [\"icon\", \"icon\"],\n [\"tooltip_text\", \"tooltip_text\"]\n ]);\n }\n render() {\n return ke`\n \n ${this.icon}\n `;\n }\n onClick(_2) {\n this.active = !this.active;\n }\n};\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"active\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"primary\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"tooltip_text\", 2);\nif (!customElements.get(ToolbarItem.componentName)) {\n customElements.define(ToolbarItem.componentName, ToolbarItem);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(ToolbarItem.componentName);\n manager.model = model;\n el.appendChild(manager);\n}\nvar toolbar_item_default = { render };\nexport {\n ToolbarItem,\n toolbar_item_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "active": false, + "icon": "image", + "layout": "IPY_MODEL_8dc8b607f2e64e47a962f0a6a9932837", + "primary": true, + "tooltip_text": "Planet imagery" + } + }, + "ac2cd52fca484632832a8ffc460f2e98": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.ToolbarItem", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/toolbar_item.ts\nvar ToolbarItem = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.active = false;\n this.primary = true;\n this.icon = \"\";\n this.tooltip_text = \"\";\n }\n static get componentName() {\n return `tool-button`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n button {\n font-size: 16px !important;\n height: 32px;\n padding: 0px 0px 0px 4px;\n width: 32px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"active\", \"active\"],\n [\"primary\", \"primary\"],\n [\"icon\", \"icon\"],\n [\"tooltip_text\", \"tooltip_text\"]\n ]);\n }\n render() {\n return ke`\n \n ${this.icon}\n `;\n }\n onClick(_2) {\n this.active = !this.active;\n }\n};\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"active\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"primary\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"tooltip_text\", 2);\nif (!customElements.get(ToolbarItem.componentName)) {\n customElements.define(ToolbarItem.componentName, ToolbarItem);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(ToolbarItem.componentName);\n manager.model = model;\n el.appendChild(manager);\n}\nvar toolbar_item_default = { render };\nexport {\n ToolbarItem,\n toolbar_item_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "active": false, + "icon": "target", + "layout": "IPY_MODEL_e3c9b813e43e46708e9a73470ba31ce6", + "primary": true, + "tooltip_text": "Get COG/STAC pixel value" + } + }, + "9b1aa26a079f4cdb933d90202e373ea5": { + "model_module": "anywidget", + "model_name": "AnyModel", + "model_module_version": "~0.9.*", + "state": { + "_anywidget_id": "geemap.toolbar.ToolbarItem", + "_dom_classes": [ + "geemap-colab" + ], + "_esm": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __decorateClass = (decorators, target, key, kind) => {\n var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;\n for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--)\n if (decorator = decorators[i4])\n result = (kind ? decorator(target, key, result) : decorator(result)) || result;\n if (kind && result) __defProp(target, key, result);\n return result;\n};\n\n// node_modules/@lit/reactive-element/css-tag.js\nvar t = globalThis;\nvar e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && \"adoptedStyleSheets\" in Document.prototype && \"replace\" in CSSStyleSheet.prototype;\nvar s = Symbol();\nvar o = /* @__PURE__ */ new WeakMap();\nvar n = class {\n constructor(t3, e5, o4) {\n if (this._$cssResult$ = true, o4 !== s) throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");\n this.cssText = t3, this.t = e5;\n }\n get styleSheet() {\n let t3 = this.o;\n const s2 = this.t;\n if (e && void 0 === t3) {\n const e5 = void 0 !== s2 && 1 === s2.length;\n e5 && (t3 = o.get(s2)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s2, t3));\n }\n return t3;\n }\n toString() {\n return this.cssText;\n }\n};\nvar r = (t3) => new n(\"string\" == typeof t3 ? t3 : t3 + \"\", void 0, s);\nvar i = (t3, ...e5) => {\n const o4 = 1 === t3.length ? t3[0] : e5.reduce((e6, s2, o5) => e6 + ((t4) => {\n if (true === t4._$cssResult$) return t4.cssText;\n if (\"number\" == typeof t4) return t4;\n throw Error(\"Value passed to 'css' function must be a 'css' function result: \" + t4 + \". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\");\n })(s2) + t3[o5 + 1], t3[0]);\n return new n(o4, t3, s);\n};\nvar S = (s2, o4) => {\n if (e) s2.adoptedStyleSheets = o4.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);\n else for (const e5 of o4) {\n const o5 = document.createElement(\"style\"), n5 = t.litNonce;\n void 0 !== n5 && o5.setAttribute(\"nonce\", n5), o5.textContent = e5.cssText, s2.appendChild(o5);\n }\n};\nvar c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {\n let e5 = \"\";\n for (const s2 of t4.cssRules) e5 += s2.cssText;\n return r(e5);\n})(t3) : t3;\n\n// node_modules/@lit/reactive-element/reactive-element.js\nvar { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;\nvar a = globalThis;\nvar c2 = a.trustedTypes;\nvar l = c2 ? c2.emptyScript : \"\";\nvar p = a.reactiveElementPolyfillSupport;\nvar d = (t3, s2) => t3;\nvar u = { toAttribute(t3, s2) {\n switch (s2) {\n case Boolean:\n t3 = t3 ? l : null;\n break;\n case Object:\n case Array:\n t3 = null == t3 ? t3 : JSON.stringify(t3);\n }\n return t3;\n}, fromAttribute(t3, s2) {\n let i4 = t3;\n switch (s2) {\n case Boolean:\n i4 = null !== t3;\n break;\n case Number:\n i4 = null === t3 ? null : Number(t3);\n break;\n case Object:\n case Array:\n try {\n i4 = JSON.parse(t3);\n } catch (t4) {\n i4 = null;\n }\n }\n return i4;\n} };\nvar f = (t3, s2) => !i2(t3, s2);\nvar y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nSymbol.metadata ??= Symbol(\"metadata\"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();\nvar b = class extends HTMLElement {\n static addInitializer(t3) {\n this._$Ei(), (this.l ??= []).push(t3);\n }\n static get observedAttributes() {\n return this.finalize(), this._$Eh && [...this._$Eh.keys()];\n }\n static createProperty(t3, s2 = y) {\n if (s2.state && (s2.attribute = false), this._$Ei(), this.elementProperties.set(t3, s2), !s2.noAccessor) {\n const i4 = Symbol(), r4 = this.getPropertyDescriptor(t3, i4, s2);\n void 0 !== r4 && e2(this.prototype, t3, r4);\n }\n }\n static getPropertyDescriptor(t3, s2, i4) {\n const { get: e5, set: h4 } = r2(this.prototype, t3) ?? { get() {\n return this[s2];\n }, set(t4) {\n this[s2] = t4;\n } };\n return { get() {\n return e5?.call(this);\n }, set(s3) {\n const r4 = e5?.call(this);\n h4.call(this, s3), this.requestUpdate(t3, r4, i4);\n }, configurable: true, enumerable: true };\n }\n static getPropertyOptions(t3) {\n return this.elementProperties.get(t3) ?? y;\n }\n static _$Ei() {\n if (this.hasOwnProperty(d(\"elementProperties\"))) return;\n const t3 = n2(this);\n t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);\n }\n static finalize() {\n if (this.hasOwnProperty(d(\"finalized\"))) return;\n if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d(\"properties\"))) {\n const t4 = this.properties, s2 = [...h(t4), ...o2(t4)];\n for (const i4 of s2) this.createProperty(i4, t4[i4]);\n }\n const t3 = this[Symbol.metadata];\n if (null !== t3) {\n const s2 = litPropertyMetadata.get(t3);\n if (void 0 !== s2) for (const [t4, i4] of s2) this.elementProperties.set(t4, i4);\n }\n this._$Eh = /* @__PURE__ */ new Map();\n for (const [t4, s2] of this.elementProperties) {\n const i4 = this._$Eu(t4, s2);\n void 0 !== i4 && this._$Eh.set(i4, t4);\n }\n this.elementStyles = this.finalizeStyles(this.styles);\n }\n static finalizeStyles(s2) {\n const i4 = [];\n if (Array.isArray(s2)) {\n const e5 = new Set(s2.flat(1 / 0).reverse());\n for (const s3 of e5) i4.unshift(c(s3));\n } else void 0 !== s2 && i4.push(c(s2));\n return i4;\n }\n static _$Eu(t3, s2) {\n const i4 = s2.attribute;\n return false === i4 ? void 0 : \"string\" == typeof i4 ? i4 : \"string\" == typeof t3 ? t3.toLowerCase() : void 0;\n }\n constructor() {\n super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();\n }\n _$Ev() {\n this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));\n }\n addController(t3) {\n (this._$EO ??= /* @__PURE__ */ new Set()).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();\n }\n removeController(t3) {\n this._$EO?.delete(t3);\n }\n _$E_() {\n const t3 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;\n for (const i4 of s2.keys()) this.hasOwnProperty(i4) && (t3.set(i4, this[i4]), delete this[i4]);\n t3.size > 0 && (this._$Ep = t3);\n }\n createRenderRoot() {\n const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);\n return S(t3, this.constructor.elementStyles), t3;\n }\n connectedCallback() {\n this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());\n }\n enableUpdating(t3) {\n }\n disconnectedCallback() {\n this._$EO?.forEach((t3) => t3.hostDisconnected?.());\n }\n attributeChangedCallback(t3, s2, i4) {\n this._$AK(t3, i4);\n }\n _$EC(t3, s2) {\n const i4 = this.constructor.elementProperties.get(t3), e5 = this.constructor._$Eu(t3, i4);\n if (void 0 !== e5 && true === i4.reflect) {\n const r4 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s2, i4.type);\n this._$Em = t3, null == r4 ? this.removeAttribute(e5) : this.setAttribute(e5, r4), this._$Em = null;\n }\n }\n _$AK(t3, s2) {\n const i4 = this.constructor, e5 = i4._$Eh.get(t3);\n if (void 0 !== e5 && this._$Em !== e5) {\n const t4 = i4.getPropertyOptions(e5), r4 = \"function\" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;\n this._$Em = e5, this[e5] = r4.fromAttribute(s2, t4.type), this._$Em = null;\n }\n }\n requestUpdate(t3, s2, i4) {\n if (void 0 !== t3) {\n if (i4 ??= this.constructor.getPropertyOptions(t3), !(i4.hasChanged ?? f)(this[t3], s2)) return;\n this.P(t3, s2, i4);\n }\n false === this.isUpdatePending && (this._$ES = this._$ET());\n }\n P(t3, s2, i4) {\n this._$AL.has(t3) || this._$AL.set(t3, s2), true === i4.reflect && this._$Em !== t3 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t3);\n }\n async _$ET() {\n this.isUpdatePending = true;\n try {\n await this._$ES;\n } catch (t4) {\n Promise.reject(t4);\n }\n const t3 = this.scheduleUpdate();\n return null != t3 && await t3, !this.isUpdatePending;\n }\n scheduleUpdate() {\n return this.performUpdate();\n }\n performUpdate() {\n if (!this.isUpdatePending) return;\n if (!this.hasUpdated) {\n if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {\n for (const [t5, s3] of this._$Ep) this[t5] = s3;\n this._$Ep = void 0;\n }\n const t4 = this.constructor.elementProperties;\n if (t4.size > 0) for (const [s3, i4] of t4) true !== i4.wrapped || this._$AL.has(s3) || void 0 === this[s3] || this.P(s3, this[s3], i4);\n }\n let t3 = false;\n const s2 = this._$AL;\n try {\n t3 = this.shouldUpdate(s2), t3 ? (this.willUpdate(s2), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s2)) : this._$EU();\n } catch (s3) {\n throw t3 = false, this._$EU(), s3;\n }\n t3 && this._$AE(s2);\n }\n willUpdate(t3) {\n }\n _$AE(t3) {\n this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);\n }\n _$EU() {\n this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;\n }\n get updateComplete() {\n return this.getUpdateComplete();\n }\n getUpdateComplete() {\n return this._$ES;\n }\n shouldUpdate(t3) {\n return true;\n }\n update(t3) {\n this._$Ej &&= this._$Ej.forEach((t4) => this._$EC(t4, this[t4])), this._$EU();\n }\n updated(t3) {\n }\n firstUpdated(t3) {\n }\n};\nb.elementStyles = [], b.shadowRootOptions = { mode: \"open\" }, b[d(\"elementProperties\")] = /* @__PURE__ */ new Map(), b[d(\"finalized\")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push(\"2.0.4\");\n\n// node_modules/lit-html/lit-html.js\nvar n3 = globalThis;\nvar c3 = n3.trustedTypes;\nvar h2 = c3 ? c3.createPolicy(\"lit-html\", { createHTML: (t3) => t3 }) : void 0;\nvar f2 = \"$lit$\";\nvar v = `lit$${Math.random().toFixed(9).slice(2)}$`;\nvar m = \"?\" + v;\nvar _ = `<${m}>`;\nvar w = document;\nvar lt = () => w.createComment(\"\");\nvar st = (t3) => null === t3 || \"object\" != typeof t3 && \"function\" != typeof t3;\nvar g = Array.isArray;\nvar $ = (t3) => g(t3) || \"function\" == typeof t3?.[Symbol.iterator];\nvar x = \"[ \t\\n\\f\\r]\";\nvar T = /<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g;\nvar E = /-->/g;\nvar k = />/g;\nvar O = RegExp(`>|${x}(?:([^\\\\s\"'>=/]+)(${x}*=${x}*(?:[^ \t\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`, \"g\");\nvar S2 = /'/g;\nvar j = /\"/g;\nvar M = /^(?:script|style|textarea|title)$/i;\nvar P = (t3) => (i4, ...s2) => ({ _$litType$: t3, strings: i4, values: s2 });\nvar ke = P(1);\nvar Oe = P(2);\nvar Se = P(3);\nvar R = Symbol.for(\"lit-noChange\");\nvar D = Symbol.for(\"lit-nothing\");\nvar V = /* @__PURE__ */ new WeakMap();\nvar I = w.createTreeWalker(w, 129);\nfunction N(t3, i4) {\n if (!g(t3) || !t3.hasOwnProperty(\"raw\")) throw Error(\"invalid template strings array\");\n return void 0 !== h2 ? h2.createHTML(i4) : i4;\n}\nvar U = (t3, i4) => {\n const s2 = t3.length - 1, e5 = [];\n let h4, o4 = 2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\", n5 = T;\n for (let i5 = 0; i5 < s2; i5++) {\n const s3 = t3[i5];\n let r4, l2, c4 = -1, a2 = 0;\n for (; a2 < s3.length && (n5.lastIndex = a2, l2 = n5.exec(s3), null !== l2); ) a2 = n5.lastIndex, n5 === T ? \"!--\" === l2[1] ? n5 = E : void 0 !== l2[1] ? n5 = k : void 0 !== l2[2] ? (M.test(l2[2]) && (h4 = RegExp(\"\" === l2[0] ? (n5 = h4 ?? T, c4 = -1) : void 0 === l2[1] ? c4 = -2 : (c4 = n5.lastIndex - l2[2].length, r4 = l2[1], n5 = void 0 === l2[3] ? O : '\"' === l2[3] ? j : S2) : n5 === j || n5 === S2 ? n5 = O : n5 === E || n5 === k ? n5 = T : (n5 = O, h4 = void 0);\n const u2 = n5 === O && t3[i5 + 1].startsWith(\"/>\") ? \" \" : \"\";\n o4 += n5 === T ? s3 + _ : c4 >= 0 ? (e5.push(r4), s3.slice(0, c4) + f2 + s3.slice(c4) + v + u2) : s3 + v + (-2 === c4 ? i5 : u2);\n }\n return [N(t3, o4 + (t3[s2] || \"\") + (2 === i4 ? \"\" : 3 === i4 ? \"\" : \"\")), e5];\n};\nvar B = class _B {\n constructor({ strings: t3, _$litType$: i4 }, s2) {\n let e5;\n this.parts = [];\n let h4 = 0, o4 = 0;\n const n5 = t3.length - 1, r4 = this.parts, [l2, a2] = U(t3, i4);\n if (this.el = _B.createElement(l2, s2), I.currentNode = this.el.content, 2 === i4 || 3 === i4) {\n const t4 = this.el.content.firstChild;\n t4.replaceWith(...t4.childNodes);\n }\n for (; null !== (e5 = I.nextNode()) && r4.length < n5; ) {\n if (1 === e5.nodeType) {\n if (e5.hasAttributes()) for (const t4 of e5.getAttributeNames()) if (t4.endsWith(f2)) {\n const i5 = a2[o4++], s3 = e5.getAttribute(t4).split(v), n6 = /([.?@])?(.*)/.exec(i5);\n r4.push({ type: 1, index: h4, name: n6[2], strings: s3, ctor: \".\" === n6[1] ? Y : \"?\" === n6[1] ? Z : \"@\" === n6[1] ? q : G }), e5.removeAttribute(t4);\n } else t4.startsWith(v) && (r4.push({ type: 6, index: h4 }), e5.removeAttribute(t4));\n if (M.test(e5.tagName)) {\n const t4 = e5.textContent.split(v), i5 = t4.length - 1;\n if (i5 > 0) {\n e5.textContent = c3 ? c3.emptyScript : \"\";\n for (let s3 = 0; s3 < i5; s3++) e5.append(t4[s3], lt()), I.nextNode(), r4.push({ type: 2, index: ++h4 });\n e5.append(t4[i5], lt());\n }\n }\n } else if (8 === e5.nodeType) if (e5.data === m) r4.push({ type: 2, index: h4 });\n else {\n let t4 = -1;\n for (; -1 !== (t4 = e5.data.indexOf(v, t4 + 1)); ) r4.push({ type: 7, index: h4 }), t4 += v.length - 1;\n }\n h4++;\n }\n }\n static createElement(t3, i4) {\n const s2 = w.createElement(\"template\");\n return s2.innerHTML = t3, s2;\n }\n};\nfunction z(t3, i4, s2 = t3, e5) {\n if (i4 === R) return i4;\n let h4 = void 0 !== e5 ? s2.o?.[e5] : s2.l;\n const o4 = st(i4) ? void 0 : i4._$litDirective$;\n return h4?.constructor !== o4 && (h4?._$AO?.(false), void 0 === o4 ? h4 = void 0 : (h4 = new o4(t3), h4._$AT(t3, s2, e5)), void 0 !== e5 ? (s2.o ??= [])[e5] = h4 : s2.l = h4), void 0 !== h4 && (i4 = z(t3, h4._$AS(t3, i4.values), h4, e5)), i4;\n}\nvar F = class {\n constructor(t3, i4) {\n this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i4;\n }\n get parentNode() {\n return this._$AM.parentNode;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n u(t3) {\n const { el: { content: i4 }, parts: s2 } = this._$AD, e5 = (t3?.creationScope ?? w).importNode(i4, true);\n I.currentNode = e5;\n let h4 = I.nextNode(), o4 = 0, n5 = 0, r4 = s2[0];\n for (; void 0 !== r4; ) {\n if (o4 === r4.index) {\n let i5;\n 2 === r4.type ? i5 = new et(h4, h4.nextSibling, this, t3) : 1 === r4.type ? i5 = new r4.ctor(h4, r4.name, r4.strings, this, t3) : 6 === r4.type && (i5 = new K(h4, this, t3)), this._$AV.push(i5), r4 = s2[++n5];\n }\n o4 !== r4?.index && (h4 = I.nextNode(), o4++);\n }\n return I.currentNode = w, e5;\n }\n p(t3) {\n let i4 = 0;\n for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t3, s2, i4), i4 += s2.strings.length - 2) : s2._$AI(t3[i4])), i4++;\n }\n};\nvar et = class _et {\n get _$AU() {\n return this._$AM?._$AU ?? this.v;\n }\n constructor(t3, i4, s2, e5) {\n this.type = 2, this._$AH = D, this._$AN = void 0, this._$AA = t3, this._$AB = i4, this._$AM = s2, this.options = e5, this.v = e5?.isConnected ?? true;\n }\n get parentNode() {\n let t3 = this._$AA.parentNode;\n const i4 = this._$AM;\n return void 0 !== i4 && 11 === t3?.nodeType && (t3 = i4.parentNode), t3;\n }\n get startNode() {\n return this._$AA;\n }\n get endNode() {\n return this._$AB;\n }\n _$AI(t3, i4 = this) {\n t3 = z(this, t3, i4), st(t3) ? t3 === D || null == t3 || \"\" === t3 ? (this._$AH !== D && this._$AR(), this._$AH = D) : t3 !== this._$AH && t3 !== R && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : $(t3) ? this.k(t3) : this._(t3);\n }\n O(t3) {\n return this._$AA.parentNode.insertBefore(t3, this._$AB);\n }\n T(t3) {\n this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));\n }\n _(t3) {\n this._$AH !== D && st(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(w.createTextNode(t3)), this._$AH = t3;\n }\n $(t3) {\n const { values: i4, _$litType$: s2 } = t3, e5 = \"number\" == typeof s2 ? this._$AC(t3) : (void 0 === s2.el && (s2.el = B.createElement(N(s2.h, s2.h[0]), this.options)), s2);\n if (this._$AH?._$AD === e5) this._$AH.p(i4);\n else {\n const t4 = new F(e5, this), s3 = t4.u(this.options);\n t4.p(i4), this.T(s3), this._$AH = t4;\n }\n }\n _$AC(t3) {\n let i4 = V.get(t3.strings);\n return void 0 === i4 && V.set(t3.strings, i4 = new B(t3)), i4;\n }\n k(t3) {\n g(this._$AH) || (this._$AH = [], this._$AR());\n const i4 = this._$AH;\n let s2, e5 = 0;\n for (const h4 of t3) e5 === i4.length ? i4.push(s2 = new _et(this.O(lt()), this.O(lt()), this, this.options)) : s2 = i4[e5], s2._$AI(h4), e5++;\n e5 < i4.length && (this._$AR(s2 && s2._$AB.nextSibling, e5), i4.length = e5);\n }\n _$AR(t3 = this._$AA.nextSibling, i4) {\n for (this._$AP?.(false, true, i4); t3 && t3 !== this._$AB; ) {\n const i5 = t3.nextSibling;\n t3.remove(), t3 = i5;\n }\n }\n setConnected(t3) {\n void 0 === this._$AM && (this.v = t3, this._$AP?.(t3));\n }\n};\nvar G = class {\n get tagName() {\n return this.element.tagName;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n constructor(t3, i4, s2, e5, h4) {\n this.type = 1, this._$AH = D, this._$AN = void 0, this.element = t3, this.name = i4, this._$AM = e5, this.options = h4, s2.length > 2 || \"\" !== s2[0] || \"\" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = D;\n }\n _$AI(t3, i4 = this, s2, e5) {\n const h4 = this.strings;\n let o4 = false;\n if (void 0 === h4) t3 = z(this, t3, i4, 0), o4 = !st(t3) || t3 !== this._$AH && t3 !== R, o4 && (this._$AH = t3);\n else {\n const e6 = t3;\n let n5, r4;\n for (t3 = h4[0], n5 = 0; n5 < h4.length - 1; n5++) r4 = z(this, e6[s2 + n5], i4, n5), r4 === R && (r4 = this._$AH[n5]), o4 ||= !st(r4) || r4 !== this._$AH[n5], r4 === D ? t3 = D : t3 !== D && (t3 += (r4 ?? \"\") + h4[n5 + 1]), this._$AH[n5] = r4;\n }\n o4 && !e5 && this.j(t3);\n }\n j(t3) {\n t3 === D ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? \"\");\n }\n};\nvar Y = class extends G {\n constructor() {\n super(...arguments), this.type = 3;\n }\n j(t3) {\n this.element[this.name] = t3 === D ? void 0 : t3;\n }\n};\nvar Z = class extends G {\n constructor() {\n super(...arguments), this.type = 4;\n }\n j(t3) {\n this.element.toggleAttribute(this.name, !!t3 && t3 !== D);\n }\n};\nvar q = class extends G {\n constructor(t3, i4, s2, e5, h4) {\n super(t3, i4, s2, e5, h4), this.type = 5;\n }\n _$AI(t3, i4 = this) {\n if ((t3 = z(this, t3, i4, 0) ?? D) === R) return;\n const s2 = this._$AH, e5 = t3 === D && s2 !== D || t3.capture !== s2.capture || t3.once !== s2.once || t3.passive !== s2.passive, h4 = t3 !== D && (s2 === D || e5);\n e5 && this.element.removeEventListener(this.name, this, s2), h4 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;\n }\n handleEvent(t3) {\n \"function\" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);\n }\n};\nvar K = class {\n constructor(t3, i4, s2) {\n this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s2;\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AI(t3) {\n z(this, t3);\n }\n};\nvar Re = n3.litHtmlPolyfillSupport;\nRe?.(B, et), (n3.litHtmlVersions ??= []).push(\"3.2.0\");\nvar Q = (t3, i4, s2) => {\n const e5 = s2?.renderBefore ?? i4;\n let h4 = e5._$litPart$;\n if (void 0 === h4) {\n const t4 = s2?.renderBefore ?? null;\n e5._$litPart$ = h4 = new et(i4.insertBefore(lt(), t4), t4, void 0, s2 ?? {});\n }\n return h4._$AI(t3), h4;\n};\n\n// node_modules/lit-element/lit-element.js\nvar h3 = class extends b {\n constructor() {\n super(...arguments), this.renderOptions = { host: this }, this.o = void 0;\n }\n createRenderRoot() {\n const t3 = super.createRenderRoot();\n return this.renderOptions.renderBefore ??= t3.firstChild, t3;\n }\n update(t3) {\n const e5 = this.render();\n this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this.o = Q(e5, this.renderRoot, this.renderOptions);\n }\n connectedCallback() {\n super.connectedCallback(), this.o?.setConnected(true);\n }\n disconnectedCallback() {\n super.disconnectedCallback(), this.o?.setConnected(false);\n }\n render() {\n return R;\n }\n};\nh3._$litElement$ = true, h3[\"finalized\"] = true, globalThis.litElementHydrateSupport?.({ LitElement: h3 });\nvar f3 = globalThis.litElementPolyfillSupport;\nf3?.({ LitElement: h3 });\n(globalThis.litElementVersions ??= []).push(\"4.1.0\");\n\n// node_modules/@lit/reactive-element/decorators/property.js\nvar o3 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };\nvar r3 = (t3 = o3, e5, r4) => {\n const { kind: n5, metadata: i4 } = r4;\n let s2 = globalThis.litPropertyMetadata.get(i4);\n if (void 0 === s2 && globalThis.litPropertyMetadata.set(i4, s2 = /* @__PURE__ */ new Map()), s2.set(r4.name, t3), \"accessor\" === n5) {\n const { name: o4 } = r4;\n return { set(r5) {\n const n6 = e5.get.call(this);\n e5.set.call(this, r5), this.requestUpdate(o4, n6, t3);\n }, init(e6) {\n return void 0 !== e6 && this.P(o4, void 0, t3), e6;\n } };\n }\n if (\"setter\" === n5) {\n const { name: o4 } = r4;\n return function(r5) {\n const n6 = this[o4];\n e5.call(this, r5), this.requestUpdate(o4, n6, t3);\n };\n }\n throw Error(\"Unsupported decorator location: \" + n5);\n};\nfunction n4(t3) {\n return (e5, o4) => \"object\" == typeof o4 ? r3(t3, e5, o4) : ((t4, e6, o5) => {\n const r4 = e6.hasOwnProperty(o5);\n return e6.constructor.createProperty(o5, r4 ? { ...t4, wrapped: true } : t4), r4 ? Object.getOwnPropertyDescriptor(e6, o5) : void 0;\n })(t3, e5, o4);\n}\n\n// node_modules/lit-html/directive.js\nvar t2 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };\nvar e4 = (t3) => (...e5) => ({ _$litDirective$: t3, values: e5 });\nvar i3 = class {\n constructor(t3) {\n }\n get _$AU() {\n return this._$AM._$AU;\n }\n _$AT(t3, e5, i4) {\n this.t = t3, this._$AM = e5, this.i = i4;\n }\n _$AS(t3, e5) {\n return this.update(t3, e5);\n }\n update(t3, e5) {\n return this.render(...e5);\n }\n};\n\n// node_modules/lit-html/directives/class-map.js\nvar Rt = e4(class extends i3 {\n constructor(s2) {\n if (super(s2), s2.type !== t2.ATTRIBUTE || \"class\" !== s2.name || s2.strings?.length > 2) throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\");\n }\n render(t3) {\n return \" \" + Object.keys(t3).filter((s2) => t3[s2]).join(\" \") + \" \";\n }\n update(t3, [s2]) {\n if (void 0 === this.st) {\n this.st = /* @__PURE__ */ new Set(), void 0 !== t3.strings && (this.nt = new Set(t3.strings.join(\" \").split(/\\s/).filter((t4) => \"\" !== t4)));\n for (const t4 in s2) s2[t4] && !this.nt?.has(t4) && this.st.add(t4);\n return this.render(s2);\n }\n const i4 = t3.element.classList;\n for (const t4 of this.st) t4 in s2 || (i4.remove(t4), this.st.delete(t4));\n for (const t4 in s2) {\n const r4 = !!s2[t4];\n r4 === this.st.has(t4) || this.nt?.has(t4) || (r4 ? (i4.add(t4), this.st.add(t4)) : (i4.remove(t4), this.st.delete(t4)));\n }\n return R;\n }\n});\n\n// js/ipywidgets_styles.ts\nvar legacyStyles = i`\n .legacy-button {\n align-items: center;\n background-color: var(--jp-layout-color2);\n border-width: 0;\n box-shadow: none;\n color: var(--jp-ui-font-color1);\n cursor: pointer;\n display: flex;\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n justify-content: center;\n line-height: var(--jp-widgets-inline-height);\n padding: 0;\n user-select: none;\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button:hover:enabled,\n .legacy-button:focus:enabled {\n box-shadow: 0 2px 2px 0\n rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 3px 1px -2px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity)),\n 0 1px 5px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity));\n }\n\n .legacy-slider {\n -webkit-appearance: none;\n appearance: none;\n background: var(--jp-layout-color3);\n border-radius: 3px;\n height: 4px;\n outline: none;\n }\n\n .legacy-slider::-webkit-slider-thumb,\n .legacy-slider::-moz-range-thumb {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n height: var(--jp-widgets-slider-handle-size);\n width: var(--jp-widgets-slider-handle-size);\n }\n\n .legacy-text {\n color: var(--jp-widgets-label-color);\n font-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n vertical-align: middle;\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: 28px;\n line-height: 28px;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-select[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .legacy-text-input {\n background: var(--jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);\n }\n\n .legacy-text-input:disabled {\n opacity: var(--jp-widgets-disabled-opacity);\n }\n\n .legacy-color {\n align-self: stretch;\n background: var(--jp-widgets-input-background-color);\n border-left: none;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex-grow: 0;\n flex-shrink: 0;\n height: var(--jp-widgets-inline-height);\n outline: none !important;\n padding: 0 2px;\n width: var(--jp-widgets-inline-height);\n }\n\n .legacy-radio {\n margin: 0;\n vertical-align: middle;\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-button.active {\n background-color: var(--colab-primary-surface-color, --jp-layout-color3);\n color: var(--jp-ui-font-color1);\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, var(--md-shadow-key-penumbra-opacity)),\n 0 1px 10px 0 rgba(0, 0, 0, var(--md-shadow-ambient-shadow-opacity)),\n 0 2px 4px -1px rgba(0, 0, 0, var(--md-shadow-key-umbra-opacity));\n }\n\n .legacy-button.primary {\n background-color: var(--jp-brand-color1);\n color: var(--jp-ui-inverse-font-color1);\n }\n\n .legacy-button.primary.active {\n background-color: var(--jp-brand-color0);\n color: var(--jp-ui-inverse-font-color0);\n }\n\n .legacy-select {\n -moz-appearance: none;\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--jp-widgets-input-background-color);\n background-image: var(--jp-widgets-dropdown-arrow);\n background-position: right center;\n background-repeat: no-repeat;\n background-size: 20px;\n border-radius: 0;\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n box-shadow: none;\n box-sizing: border-box;\n color: var(--jp-widgets-input-color);\n flex: 1 1 var(--jp-widgets-inline-width-short);\n font-size: var(--jp-widgets-font-size);\n height: inherit;\n min-width: 0;\n outline: none !important;\n padding-left: calc(var(--jp-widgets-input-padding)* 2);\n padding-right: 20px;\n vertical-align: top;\n }\n\n .legacy-input {\n box-sizing: border-box;\n background-color: var(--colab-primary-surface-color, --jp-widgets-input-background-color);\n border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);\n color: var(--jp-widgets-input-color);\n flex-grow: 1;\n flex-shrink: 1;\n font-size: var(--jp-widgets-font-size);\n min-width: 0;\n outline: none !important;\n padding: var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding)* 2);\n height: var(--jp-widgets-inline-height);\n line-height: var(--jp-widgets-inline-height);\n }\n`;\n\n// js/utils.ts\nfunction loadFonts() {\n if (!document.querySelector(\".custom-fonts\")) {\n const styleElement = document.createElement(\"style\");\n styleElement.classList.add(\"custom-fonts\");\n styleElement.textContent = '@import \"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined\";';\n document.body.appendChild(styleElement);\n }\n}\nfunction reverseMap(map) {\n const reversedMap = /* @__PURE__ */ new Map();\n for (const [key, value] of map.entries()) {\n if (value != null) {\n reversedMap.set(value, key);\n }\n }\n return reversedMap;\n}\n\n// js/lit_widget.ts\nvar LitWidget = class extends h3 {\n constructor() {\n super(...arguments);\n this._model = void 0;\n }\n onCustomMessage(_msg) {\n }\n viewNameToModelName() {\n return reverseMap(this.modelNameToViewName());\n }\n set model(model) {\n this._model = model;\n for (const [modelKey, widgetKey] of this.modelNameToViewName()) {\n if (widgetKey) {\n this[widgetKey] = model.get(modelKey);\n model.on(`change:${String(modelKey)}`, () => {\n this[widgetKey] = model.get(modelKey);\n });\n }\n }\n model.on(\"msg:custom\", (msg) => {\n this.onCustomMessage?.(msg);\n });\n }\n get model() {\n return this._model;\n }\n updated(changedProperties) {\n const viewToModelMap = this.viewNameToModelName();\n for (const [viewProp, _2] of changedProperties) {\n const castViewProp = viewProp;\n if (viewToModelMap.has(castViewProp)) {\n const modelProp = viewToModelMap.get(castViewProp);\n this._model?.set(\n modelProp,\n this[castViewProp]\n );\n }\n }\n this._model?.save_changes();\n }\n};\n\n// js/styles.ts\nvar materialStyles = i`\n @font-face {\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v205/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2)\n format(\"woff2\");\n }\n\n .material-symbols-outlined {\n -webkit-font-feature-settings: \"liga\";\n -webkit-font-smoothing: antialiased;\n direction: ltr;\n display: inline-block;\n font-family: \"Material Symbols Outlined\";\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n text-transform: none;\n white-space: nowrap;\n word-wrap: normal;\n }\n`;\nvar flexStyles = i`\n .vertical-flex {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .horizontal-flex {\n align-items: center;\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n }\n\n input:not([type=\"radio\"]):not([type=\"checkbox\"]) {\n width: 100%;\n }\n\n select {\n width: 100%;\n }\n\n .horizontal-flex .legacy-text {\n flex-shrink: 0;\n }\n`;\n\n// js/toolbar_item.ts\nvar ToolbarItem = class extends LitWidget {\n constructor() {\n super(...arguments);\n this.active = false;\n this.primary = true;\n this.icon = \"\";\n this.tooltip_text = \"\";\n }\n static get componentName() {\n return `tool-button`;\n }\n static {\n this.styles = [\n legacyStyles,\n materialStyles,\n i`\n button {\n font-size: 16px !important;\n height: 32px;\n padding: 0px 0px 0px 4px;\n width: 32px;\n }\n `\n ];\n }\n modelNameToViewName() {\n return /* @__PURE__ */ new Map([\n [\"active\", \"active\"],\n [\"primary\", \"primary\"],\n [\"icon\", \"icon\"],\n [\"tooltip_text\", \"tooltip_text\"]\n ]);\n }\n render() {\n return ke`\n \n ${this.icon}\n `;\n }\n onClick(_2) {\n this.active = !this.active;\n }\n};\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"active\", 2);\n__decorateClass([\n n4({ type: Boolean })\n], ToolbarItem.prototype, \"primary\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"icon\", 2);\n__decorateClass([\n n4({ type: String })\n], ToolbarItem.prototype, \"tooltip_text\", 2);\nif (!customElements.get(ToolbarItem.componentName)) {\n customElements.define(ToolbarItem.componentName, ToolbarItem);\n}\nasync function render({ model, el }) {\n loadFonts();\n const manager = document.createElement(ToolbarItem.componentName);\n manager.model = model;\n el.appendChild(manager);\n}\nvar toolbar_item_default = { render };\nexport {\n ToolbarItem,\n toolbar_item_default as default\n};\n/*! Bundled license information:\n\n@lit/reactive-element/css-tag.js:\n (**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/reactive-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/lit-html.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-element/lit-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/is-server.js:\n (**\n * @license\n * Copyright 2022 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/custom-element.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/property.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/state.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/event-options.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/base.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-all.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-async.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-elements.js:\n (**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\n@lit/reactive-element/decorators/query-assigned-nodes.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directive.js:\n (**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n\nlit-html/directives/class-map.js:\n (**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *)\n*/\n", + "_model_module": "anywidget", + "_model_module_version": "~0.9.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.9.*", + "_view_name": "AnyView", + "active": false, + "icon": "question_mark", + "layout": "IPY_MODEL_13e1ae2b10a04cf9a8dfb8bcdfda78b9", + "primary": true, + "tooltip_text": "Get help" + } + }, + "7dc38477ca5145948632181e660a72ca": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3117c8f8dc454974ad725820bbaff1b6": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "f306390cbeee4c17acad2a7faea2bda7": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "bd44dc656406487e96cbe194c6261bd0": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e9f6bd2414c84f258c433913fe8cf03e": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "f0268b4fa5f8426aab5467c9b24a5fdd": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "c5f72af9d368407dbaf5d9e4d7b91392": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "91e3f52fd7364d319305e5af41f1c82c": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "125e8e3b0ac04f6993b13c51a0380138": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ea343d9e0bb749998ee948e3b48e087f": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8038eb785016462b8c9b593921119002": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "fbb8c80ca0784cef9d6139a5fbbb6b46": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "49cfc07507794d6aab0419f0f0f17ef4": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "44d6efda35304e3b82becaa0554cc00a": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "a386181ab2a948f39200afa36498a19e": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "313ce92ad01147d1bd2f3e3491f0fabf": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b6f2cd884b4140c2b954178b4a3f5ffb": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8dc8b607f2e64e47a962f0a6a9932837": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e3c9b813e43e46708e9a73470ba31ce6": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "13e1ae2b10a04cf9a8dfb8bcdfda78b9": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + } + } + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/utilities/stac_spec_collector.py b/utilities/stac_spec_collector.py new file mode 100644 index 00000000..1858b7ce --- /dev/null +++ b/utilities/stac_spec_collector.py @@ -0,0 +1,93 @@ +import json +import os + +from computing.STAC_specs.stac_collection import STACConfig, sanitize_text + + +def _read_json(path): + if not os.path.exists(path): + return None + with open(path) as f: + return json.load(f) + + +def build_stac_item_id(state, district, block, layer_name, year=""): + parts = [state, district, block, layer_name] + if year not in (None, ""): + parts.append(str(year)) + return "_".join(parts) + + +def collect_generated_stac_specs( + *, + state, + district, + block, + layer_name, + layer_type, + start_year="", + end_year="", +): + """Return STAC catalog metadata and items scoped to a single layer.""" + state = sanitize_text(str(state).lower()) + district = sanitize_text(str(district).lower()) + block = sanitize_text(str(block).lower()) + layer_name = sanitize_text(str(layer_name).lower()) + + stac_config = STACConfig() + tehsil_dirname = stac_config.tehsil_dirname + base_dir = stac_config.stac_files_dir + tehsil_dir = os.path.join(base_dir, tehsil_dirname) + state_dir = os.path.join(tehsil_dir, state) + district_dir = os.path.join(state_dir, district) + block_dir = os.path.join(district_dir, block) + + root_catalog = _read_json(os.path.join(base_dir, "catalog.json")) + tehsil_catalog = _read_json(os.path.join(tehsil_dir, "catalog.json")) + state_collection = _read_json(os.path.join(state_dir, "collection.json")) + district_collection = _read_json(os.path.join(district_dir, "collection.json")) + block_collection = _read_json(os.path.join(block_dir, "collection.json")) + + requested_item_ids = [] + if layer_type == "raster" and str(start_year).strip() and str(end_year).strip(): + requested_item_ids = [ + build_stac_item_id(state, district, block, layer_name, str(y)) + for y in range(int(start_year), int(end_year) + 1) + ] + elif str(start_year).strip(): + requested_item_ids = [ + build_stac_item_id( + state, district, block, layer_name, str(start_year).strip() + ) + ] + else: + requested_item_ids = [ + build_stac_item_id(state, district, block, layer_name, "") + ] + + item_prefix = build_stac_item_id(state, district, block, layer_name, "") + discovered_item_ids = [] + if os.path.isdir(block_dir): + for entry in os.listdir(block_dir): + if entry.startswith(item_prefix) and os.path.isdir( + os.path.join(block_dir, entry) + ): + discovered_item_ids.append(entry) + + item_ids = sorted(set(requested_item_ids + discovered_item_ids)) + + items = [] + for item_id in item_ids: + item_path = os.path.join(block_dir, item_id, f"{item_id}.json") + item_spec = _read_json(item_path) + if item_spec is not None: + items.append(item_spec) + + return { + "root_catalog": root_catalog, + "tehsil_catalog": tehsil_catalog, + "state_collection": state_collection, + "district_collection": district_collection, + "block_collection": block_collection, + "items": items, + } diff --git a/waterrejuvenation/utils.py b/waterrejuvenation/utils.py index 667d318a..8ec75698 100644 --- a/waterrejuvenation/utils.py +++ b/waterrejuvenation/utils.py @@ -832,7 +832,7 @@ def add_on_drainage_flag(swb_fc, dl_asset_id): ee.FeatureCollection: SWB FC with added property 'on_drainage_line' """ - dl_fc = ee.FeatureCollection(dl_asset_id) + dl_fc = ee.FeatureCollection(dl_asset_id).filterBounds(swb_fc.geometry()) # Map over each SWB feature def set_flag(feature):