diff --git a/cloud_functions/sustainable_sourcing_layers/main.py b/cloud_functions/sustainable_sourcing_layers/main.py new file mode 100644 index 0000000..70155ba --- /dev/null +++ b/cloud_functions/sustainable_sourcing_layers/main.py @@ -0,0 +1,42 @@ +import json +import ee +from flask import jsonify +import functions_framework +import logging +import os +import requests + +import google.auth +import google.cloud.logging + +from suso_layers_2025a import get_suso_stats + +client = google.cloud.logging.Client() +client.setup_logging() + +@functions_framework.http +def main(request): + """Handle requests in a format (geojson) suitable for BigQuery.""" + credentials, _ = google.auth.default( + scopes=['https://www.googleapis.com/auth/earthengine'] + ) + ee.Initialize(credentials, project=os.environ['GOOGLE_CLOUD_PROJECT']) + try: + replies = [] + request_json = request.get_json(silent=True) + calls = request_json['calls'] + for call in calls: + geo_json = json.loads(call[0]) + try: + logging.info([geo_json]) + response = get_suso_stats(geo_json) + logging.info(response) + replies.append(json.dumps(response)) + except Exception as e: + logging.error(str(e)) + replies.append(json.dumps( { "errorMessage": str(e) } )) + return jsonify(replies=replies, status=200, mimetype='application/json') + except Exception as e: + error_string = str(e) + logging.error(error_string) + return jsonify(error=error_string, status=400, mimetype='application/json') diff --git a/cloud_functions/sustainable_sourcing_layers/requirements.txt b/cloud_functions/sustainable_sourcing_layers/requirements.txt new file mode 100644 index 0000000..648703e --- /dev/null +++ b/cloud_functions/sustainable_sourcing_layers/requirements.txt @@ -0,0 +1,6 @@ +earthengine-api +flask +functions-framework +google-api-core +google-cloud-logging +requests diff --git a/cloud_functions/sustainable_sourcing_layers/suso_layers_2025a.py b/cloud_functions/sustainable_sourcing_layers/suso_layers_2025a.py new file mode 100644 index 0000000..489c97c --- /dev/null +++ b/cloud_functions/sustainable_sourcing_layers/suso_layers_2025a.py @@ -0,0 +1,112 @@ +import os + +import ee +import google.auth +from google.api_core import retry + +# First, initialize. +credentials, _ = google.auth.default( + scopes=['https://www.googleapis.com/auth/earthengine'] +) +ee.Initialize( + credentials, + project=os.environ['GOOGLE_CLOUD_PROJECT'], + opt_url='https://earthengine-highvolume.googleapis.com' +) + +# See https://github.com/google/forest-data-partnership/tree/main/models. +cocoa_2025a = ee.ImageCollection('projects/forestdatapartnership/assets/cocoa/model_2025a') +coffee_2025a = ee.ImageCollection('projects/forestdatapartnership/assets/coffee/model_2025a') +palm_2025a = ee.ImageCollection('projects/forestdatapartnership/assets/palm/model_2025a') +rubber_2025a = ee.ImageCollection('projects/forestdatapartnership/assets/rubber/model_2025a') + +filter2020 = ee.Filter.calendarRange(2020, 2020, 'year') +filter2023 = ee.Filter.calendarRange(2023, 2023, 'year') + +cocoa2020 = cocoa_2025a.filter(filter2020).mosaic().rename('cocoa_2020') +cocoa2023 = cocoa_2025a.filter(filter2023).mosaic().rename('cocoa_2023') +coffee2020 = coffee_2025a.filter(filter2020).mosaic().rename('coffee_2020') +coffee2023 = coffee_2025a.filter(filter2023).mosaic().rename('coffee_2023') +palm2020 = palm_2025a.filter(filter2020).mosaic().rename('palm_2020') +palm2023 = palm_2025a.filter(filter2023).mosaic().rename('palm_2023') +rubber2020 = rubber_2025a.filter(filter2020).mosaic().rename('rubber_2020') +rubber2023 = rubber_2025a.filter(filter2023).mosaic().rename('rubber_2023') + +# See https://eartharxiv.org/repository/view/9085/. +natural_forest2020 = ee.ImageCollection( + 'projects/computing-engine-190414/assets/biosphere_models/public/forest_typology/natural_forest_2020_v1_0' + ).mosaic().divide(255).selfMask() + +# THRESHOLDS FOR DEMONSTRATION ONLY! Tune these to your needs. +thresholds = { + 'forest': 0.5, + 'cocoa': 0.45, + 'coffee': 0.96, + 'palm': 0.89, + 'rubber': 0.5 +} + +# A mini-ensemble of GDM and fodapa data products. +ensemble = ee.Image.cat( + natural_forest2020.rename('forest'), + cocoa2020.rename('cocoa'), + coffee2020.rename('coffee'), + palm2020.rename('palm'), + rubber2020.rename('rubber') +).unmask(0) + +# Threshold the probabilities. THRESHOLDS FOR DEMONSTRATION ONLY! +crop_names = list(thresholds.keys()) +threshold_values = list(thresholds.values()) +thresholded = ensemble.select(crop_names).gt(ee.Image(threshold_values)) + +# Unclassified means no predicted presence at the specified thresholds. +unclassified = thresholded.reduce('sum').eq(0).rename('unclassified') + +# Confusion means two or more classes predicted presence. +confusion = thresholded.reduce('sum').gt(1).selfMask().rename('confusion') + +def get_suso_layers_2025a() -> ee.Image: + '''Returns the stack of probability images in separate bands.''' + return ee.Image.cat( + natural_forest2020.rename('natural_forest_2020'), + cocoa2020.rename('cocoa_probability_2020'), + cocoa2023.rename('cocoa_probability_2023'), + coffee2020.rename('coffee_probability_2020'), + coffee2023.rename('coffee_probability_2023'), + palm2020.rename('palm_probability_2020'), + palm2023.rename('palm_probability_2023'), + rubber2020.rename('rubber_probability_2020'), + rubber2023.rename('rubber_probability_2023'), + ) + +def get_areas_image() -> ee.Image: + '''Returns data for area calculations in square meters.''' + return ee.Image.cat( + thresholded, + unclassified.rename('unclassified'), + confusion.rename('confusion') + ).multiply(ee.Image.pixelArea()) + +@retry.Retry() +def get_suso_stats(geojson): + """Get area stats for the provided geojson polygon.""" + region = ee.Geometry(geojson) + feature_area = ee.Number(region.area(10)) + suso_image = get_areas_image() + # Sum of pixel areas in square meters. + stats = suso_image.reduceRegion( + reducer=ee.Reducer.sum(), + geometry=region, + scale=10 + ) + # Gini index. + # See https://en.wikipedia.org/wiki/Decision_tree_learning#Gini_impurity. + crop_names = ['forest', 'cocoa', 'coffee', 'palm', 'rubber'] + gini = ee.Number(1).subtract(ee.List( + [ee.Number(stats.get(c)).divide(feature_area) for c in crop_names] + ).reduce(ee.Reducer.sum())) + # Update the EE dictionary. + stats = stats.set('gini', gini).set('total_area', feature_area) + # Request the result to the client and return it. + return stats.getInfo() \ No newline at end of file diff --git a/notebooks/sustainable_sourcing_layers_cloud_function.ipynb b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb new file mode 100644 index 0000000..1c79934 --- /dev/null +++ b/notebooks/sustainable_sourcing_layers_cloud_function.ipynb @@ -0,0 +1,774 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "source": [ + "# Sustainable Sourcing Layers in a Cloud Function\n", + "\n", + "This notebook demonstrates deployment of Google Cloud functions to get commodity and forest information in a format designed to be integrated to other workflows.\n", + "\n", + "The 2025a release of Forest Data Partnership sustainable sourcing layers including palm, rubber, cocoa and coffee are used for commodity information. For details on how these layers were produced, see [the technical documentation on GitHub](https://github.com/google/forest-data-partnership/tree/main/models). In particular, see [the limitations](https://github.com/google/forest-data-partnership/tree/main/models#limitations). See also the [Forest Data Partnership publisher catalog](https://developers.google.com/earth-engine/datasets/publisher/forestdatapartnership) for dataset descriptions. See [this Earth Engine Code Editor script](https://goo.gle/fodapa-layers) for a demonstration of how choice of thresholds affects the mapped results.\n", + "\n", + "Note that users of commercial projects will need to request access to the Forest Data Partnership datasets with [this form](https://docs.google.com/forms/d/e/1FAIpQLSe7L3eh6t2JIPqEtAQwXwY7ZmW52v8W5vrIi4QN_XYgTNJZLw/viewform).\n", + "\n", + "**WARNING**: These demos consume billable resources and may result in charges to your account!" + ], + "metadata": { + "id": "RDYfoFn6e0G3" + } + }, + { + "cell_type": "markdown", + "source": [ + "# Setup" + ], + "metadata": { + "id": "yaw0K_p2ieqt" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Set the Colab Environment environment variables\n", + "\n", + "Import the Python libraries used by the section." + ], + "metadata": { + "id": "vbXOEZtIe7tb" + } + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dUTBm0ETVj3u" + }, + "outputs": [], + "source": [ + "import json\n", + "import os\n", + "\n", + "from google.colab import userdata" + ] + }, + { + "cell_type": "markdown", + "source": [ + "This notebook assumes that the GCP project and compute region information are stored as Colab secrets. This avoids storing user-specific and potentially sensitive information in the notebook. See this guide for information on how to use Colab secrets." + ], + "metadata": { + "id": "iYkUyHrQfFLF" + } + }, + { + "cell_type": "code", + "source": [ + "# Load from Colab secrets\n", + "PROJECT = userdata.get('GOOGLE_CLOUD_PROJECT')\n", + "REGION = userdata.get('GOOGLE_CLOUD_LOCATION')\n", + "\n", + "# Store as environment variables\n", + "os.environ['GOOGLE_CLOUD_PROJECT'] = PROJECT\n", + "os.environ['GOOGLE_CLOUD_LOCATION'] = REGION" + ], + "metadata": { + "id": "isACEYGufCQL" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Authenticate\n", + "\n", + "Authenticate using the Google Cloud CLI (gcloud)." + ], + "metadata": { + "id": "gadkSfi5fLrJ" + } + }, + { + "cell_type": "code", + "source": [ + "!gcloud auth login --project {PROJECT} --billing-project {PROJECT} --update-adc" + ], + "metadata": { + "id": "M6Y6XiiJfIPq" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Load WHISP example data\n", + "\n", + "We will test out the Cloud Run function using some example data from\n", + "[WHISP](xhttps://openforis.org/solutions/whisp/).\n", + "\n", + "Start by downloading some WHISP example data from GitHub and convert it to an `ee.FeatureCollection`." + ], + "metadata": { + "id": "8VPkk3LBkGhf" + } + }, + { + "cell_type": "code", + "source": [ + "fc_list = !curl https://raw.githubusercontent.com/forestdatapartnership/whisp/main/tests/fixtures/geojson_example.geojson\n", + "fc_obj = json.loads(\"\\n\".join(fc_list))\n", + "features = fc_obj['features']" + ], + "metadata": { + "id": "bPd9lD_bkQkW" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Print an example feature." + ], + "metadata": { + "id": "d4cKpH8RkcPr" + } + }, + { + "cell_type": "code", + "source": [ + "features[0]" + ], + "metadata": { + "id": "dlRGneG0kdCM" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Extract the feature geometries." + ], + "metadata": { + "id": "vz10es_pkwNJ" + } + }, + { + "cell_type": "code", + "source": [ + "geoms = [f['geometry'] for f in features]" + ], + "metadata": { + "id": "_1lziQOukw6C" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Print an example geometry.\n" + ], + "metadata": { + "id": "pfKL-XBYkzY1" + } + }, + { + "cell_type": "code", + "source": [ + "geoms[0]" + ], + "metadata": { + "id": "1KTGwy3uk5UT" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Create the Cloud Run function" + ], + "metadata": { + "id": "xewQ7PFghhuY" + } + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KoxarHascAIB" + }, + "source": [ + "## Checkout the Cloud Run function source code\n", + "\n", + "Cloud Run functions are defined from a collection of files in a [source directory](https://cloud.google.com/run/docs/write-functions#directory_structure). This section downloads Cloud Run function definition files to Colab's storage using git's [sparse-checkout](https://git-scm.com/docs/git-sparse-checkout)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "n8nuSnu2U_hQ" + }, + "outputs": [], + "source": [ + "%%bash\n", + "is_git_repo() {\n", + " git rev-parse --is-inside-work-tree >/dev/null 2>&1\n", + "}\n", + "\n", + "if is_git_repo; then\n", + " echo \"This is already a git repository. No action was taken.\"\n", + "else\n", + " echo \"This is not a git repository. Initiating sparse checkout...\"\n", + " repo_url=https://github.com/tylere/forest-data-partnership.git\n", + " branch=cloud-function-folder\n", + " cloud_function_source_dir=cloud_functions/sustainable_sourcing_layers\n", + "\n", + " # Use git sparse-checkout\n", + " git config --global init.defaultBranch $branch\n", + " git init\n", + " git remote add origin $repo_url\n", + " git sparse-checkout init --cone\n", + " git sparse-checkout set $cloud_function_source_dir\n", + " git pull origin $branch\n", + "fi" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Test the Cloud Run function code\n", + "\n", + "Import the code and run a few tests locally." + ], + "metadata": { + "id": "R4cAROMhhCyX" + } + }, + { + "cell_type": "code", + "source": [ + "from pprint import pprint\n", + "import ee\n", + "\n", + "print('Using EE version: ', ee.__version__)" + ], + "metadata": { + "id": "U7FynjCmvvKl" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "from cloud_functions.sustainable_sourcing_layers import suso_layers_2025a\n", + "\n", + "# Check the image metadata and bands.\n", + "layers = suso_layers_2025a.get_suso_layers_2025a()\n", + "pprint(layers.getInfo())" + ], + "metadata": { + "id": "rquPJFwzlf1x" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Sample the layers at a specified point." + ], + "metadata": { + "id": "qApFqpfHmfO4" + } + }, + { + "cell_type": "code", + "source": [ + "test_point = ee.Geometry.Point(104.33, -3.41)\n", + "pprint(layers.reduceRegion(ee.Reducer.mean(), test_point, 10).getInfo())" + ], + "metadata": { + "id": "FBk5a_V8hI8l" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Get the class areas at a specific point" + ], + "metadata": { + "id": "xjsDiE8xnCzw" + } + }, + { + "cell_type": "code", + "source": [ + "print(\n", + " suso_layers_2025a\n", + " .get_areas_image()\n", + " .reduceRegion(\n", + " ee.Reducer.mean(),\n", + " test_point,\n", + " 10\n", + " ).getInfo()\n", + ")" + ], + "metadata": { + "id": "-2wJQYcYlGd3" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Get statistics within an example geometry." + ], + "metadata": { + "id": "r1SnGr7_nR5s" + } + }, + { + "cell_type": "code", + "source": [ + "suso_layers_2025a.get_suso_stats(geojson=geoms[0])" + ], + "metadata": { + "id": "5a89qkMeiMqQ" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Deploy the Cloud Run function\n", + "\n", + "See [this quickstart](https://cloud.google.com/run/docs/quickstarts/functions/deploy-functions-gcloud) for details on deploying Cloud Run functions.\n", + "\n", + "The Cloud Run function configuration files are stored in the folder `cloud_functions/sustainable_sourcing_layers`." + ], + "metadata": { + "id": "f8DwusJvnk0f" + } + }, + { + "cell_type": "markdown", + "source": [ + "Inspect the list of Python packages required to run the Cloud Run function." + ], + "metadata": { + "id": "2VLBq9bwh1bQ" + } + }, + { + "cell_type": "code", + "source": [ + "%cat cloud_functions/sustainable_sourcing_layers/requirements.txt" + ], + "metadata": { + "id": "6gJ5JXfbfOKQ" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Use gcloud to Deploy the Cloud Run function\n", + "\n", + "Use the following `gcloud` command to deploy the Cloud Run function.\n", + "\n", + "*Note that the Compute Engine default service account is being used for authentication to Earth Engine. For commercial access to the sustainable sourcing layers, ensure that the service account is approved for commercial access ([request form](https://docs.google.com/forms/d/e/1FAIpQLSe7L3eh6t2JIPqEtAQwXwY7ZmW52v8W5vrIi4QN_XYgTNJZLw/viewform)).*" + ], + "metadata": { + "id": "Lur5Fe7SoyIy" + } + }, + { + "cell_type": "code", + "source": [ + "!gcloud functions deploy 'suso_function' \\\n", + " --gen2 \\\n", + " --region={REGION} \\\n", + " --project={PROJECT} \\\n", + " --runtime=python312 \\\n", + " --source='cloud_functions/sustainable_sourcing_layers' \\\n", + " --set-env-vars GOOGLE_CLOUD_PROJECT={PROJECT} \\\n", + " --entry-point=main \\\n", + " --trigger-http \\\n", + " --no-allow-unauthenticated \\\n", + " --timeout=300s" + ], + "metadata": { + "id": "9G96vr2io_gQ" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + " It's helpful to follow the link in the deployment output to monitor and/or debug your Cloud Run function. In particular, see the metrics and logs tabs to help resolve perfpormance issues." + ], + "metadata": { + "id": "8YaUZnYqo8OK" + } + }, + { + "cell_type": "markdown", + "source": [ + "# Call the deployed Cloud Run function\n", + "\n", + "This section demonstrates how to call the deployed Cloud Run function using different tools.\n", + "\n", + "*(Note that after creating a new Colab runtime you must first run the [Setup](#scrollTo=yaw0K_p2ieqt) section before running this section.)*" + ], + "metadata": { + "id": "ORIfMTYTp-XC" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Call using curl\n", + "\n", + "The section demonstrates calling Cloud Run function using [curl](https://curl.se/), a general purpose command-line tool used to transfer data to or from a server using a wide range of network protocols." + ], + "metadata": { + "id": "ZQVTTrJrgDhs" + } + }, + { + "cell_type": "markdown", + "source": [ + "Create a JSON formatted string of the Cloud Run function inputs (i.e. WHISP sample geometries)." + ], + "metadata": { + "id": "_5hPKl6jq1VQ" + } + }, + { + "cell_type": "code", + "source": [ + "payload = {\n", + " \"calls\": [[json.dumps(g)] for g in geoms]\n", + "}\n", + "payload_json = json.dumps(payload, separators=(',', ':'))\n", + "payload_json" + ], + "metadata": { + "id": "i01h0RTwqqXF" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Format the payload for curl, by wrapping it in escaped single quotes." + ], + "metadata": { + "id": "87sKKIDkqtv7" + } + }, + { + "cell_type": "code", + "source": [ + "post_data = f\"'{payload_json}'\"\n", + "post_data" + ], + "metadata": { + "id": "4jxcpFeTsfHx" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Use curl to submit a test POST data. This might take a few seconds to execute." + ], + "metadata": { + "id": "Qo1cRl_wrBpg" + } + }, + { + "cell_type": "code", + "source": [ + "response_body_text_list = !curl -X POST https://{REGION}-{PROJECT}.cloudfunctions.net/suso_function \\\n", + " -H \"Authorization: bearer $(gcloud auth print-identity-token)\" \\\n", + " -H \"Content-Type: application/json\" \\\n", + " -d {post_data}\n", + "response_body_text_list" + ], + "metadata": { + "id": "VTZqNgfRq2jV" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Use Python to parse and inspect the Cloud Run function response." + ], + "metadata": { + "id": "bTeBYf12rJkI" + } + }, + { + "cell_type": "code", + "source": [ + "response = response_body_text_list[0]\n", + "response_json = json.loads(response)\n", + "replies_json = [json.loads(reply) for reply in response_json['replies']]\n", + "reply_keys = replies_json[0].keys()\n", + "\n", + "print(f'{len(replies_json) = }')\n", + "print(f'{reply_keys = }')" + ], + "metadata": { + "id": "rYv1u1qrrEI2" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Pretty print the replies." + ], + "metadata": { + "id": "FrE4tZ-ZraDJ" + } + }, + { + "cell_type": "code", + "source": [ + "print(json.dumps(replies_json, indent=4))" + ], + "metadata": { + "id": "WrlloZKBrOu5" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Call using Python" + ], + "metadata": { + "id": "nRtdxiW9Uz3K" + } + }, + { + "cell_type": "code", + "source": [ + "import google.auth.transport.requests\n", + "import requests" + ], + "metadata": { + "id": "t305EBW_dF8f" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Retrieve your Google Cloud account identity token." + ], + "metadata": { + "id": "c1matw9LVHJU" + } + }, + { + "cell_type": "code", + "source": [ + "# Set this to the target audience (Cloud Run service URL)\n", + "audience = \"https://us-west1-vorgeo-training.cloudfunctions.net/suso_function\"\n", + "\n", + "project = os.environ['GOOGLE_CLOUD_PROJECT']\n", + "credentials, _ = google.auth.default(\n", + " scopes=['https://www.googleapis.com/auth/earthengine'],\n", + ")\n", + "credentials.refresh(\n", + " request = google.auth.transport.requests.Request()\n", + ")\n", + "identity_token = credentials.id_token\n", + "\n", + "print(f'{identity_token = }')" + ], + "metadata": { + "id": "LcqZ5dDqWR1N" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Make a call to the Cloud Run function." + ], + "metadata": { + "id": "oijzeN0OdH6g" + } + }, + { + "cell_type": "code", + "source": [ + "headers = {\n", + " \"Authorization\": f\"Bearer {identity_token}\",\n", + " \"Content-Type\": \"application/json\"\n", + "}\n", + "\n", + "# Prepare the payload in the format expected by the Cloud Function\n", + "payload = {\n", + " \"calls\": [[json.dumps(g)] for g in geoms]\n", + "}\n", + "\n", + "response = requests.post(audience, headers=headers, json=payload)\n", + "print(response.text)" + ], + "metadata": { + "id": "8ALAR16kc0E1" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Parse the Cloud Run function response." + ], + "metadata": { + "id": "1vc0WRLvfeyc" + } + }, + { + "cell_type": "code", + "source": [ + "response_json = json.loads(response.text)\n", + "replies_json = [json.loads(reply) for reply in response_json['replies']]\n", + "print(json.dumps(replies_json, indent=4))" + ], + "metadata": { + "id": "k0SAf0iHde7q" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "### Display the features" + ], + "metadata": { + "id": "RhCObMj6zWdo" + } + }, + { + "cell_type": "markdown", + "source": [ + "For each feature, update the properties to include the Cloud Run function results." + ], + "metadata": { + "id": "LLlcdooTqW0y" + } + }, + { + "cell_type": "code", + "source": [ + "for i in range(len(features)):\n", + " features[i]['properties'].update(replies_json[i])" + ], + "metadata": { + "id": "ftKrM7Sgs7A-" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Display the updated features on a map (styled red). Click on the features to inspect their properties." + ], + "metadata": { + "id": "RkUaGjpLy2Ps" + } + }, + { + "cell_type": "code", + "source": [ + "import geemap\n", + "\n", + "Map = geemap.Map(center=(1.9332, 6.3830), zoom=5)\n", + "Map.add_basemap('SATELLITE')\n", + "Map.add_inspector()\n", + "Map.addLayer(ee.FeatureCollection(features), {'color': 'FF0000'}, 'Features')\n", + "Map" + ], + "metadata": { + "id": "8-yRpjIwte9h" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Next Steps\n", + "\n", + "- Try the [WHISP Cloud Function demo notebook](https://colab.research.google.com/drive/1NCaPOoxqmAEWb8c8V0kEHVunbW1yHVkL?resourcekey=0-HJ3ou94AbjdKkkvaPW1Jtw&usp=sharing)." + ], + "metadata": { + "id": "7QPlRfQe22Qm" + } + } + ], + "metadata": { + "colab": { + "provenance": [], + "toc_visible": true, + "include_colab_link": true + }, + "kernelspec": { + "display_name": "default", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file