Geospatial data

This section describes the geospatial data that is generated by the Chloris Platform and how to access it. Because geospatial data can be confusing, we'll start with a brief introduction to the concepts and terminology. Then cover input datasets, output datasets, and how to access them.

Geospatial data concepts

Geospatial data is any data that has a location component, specifically on Earth. It is separated into two distinct classes: vector and raster. Vector datasets are composed of points (such as longitude and latitude), lines, and polygons. Raster datasets are composed of pixels, possibly with multiple bands, which store more than one value for a single grid cell.

The Chloris Platform consumes vector datasets in the form of site boundaries, and produces raster output datasets. One special thing about geospatial datasets that is not immediately obvious is that every geospatial dataset must have a Coordinate Reference System (CRS), sometimes called a "projection", which describes where the data is located on Earth. The CRS is usually specified in the metadata of the dataset, but certain formats (such as GeoJSON) assume a CRS of WGS84 (EPSG:4326) if one is not specified. Without the correct CRS, the data will not be located in the correct place on Earth.

The Chloris Platform also produces tabular data (i.e. CSV files), but this is not generally considered to be geospatial data. There is one notable exception, which is when the tabular data contains a location component (e.g. longitude and latitude) as we sometimes see with field plots around a central point. Currently, the Chloris Platform does not consume field data, so within the platform interface, you will only see tabular data in the form of graphical tables or CSV files.

Additional beginner geospatial resources:

Input datasets

Site boundaries

Site boundaries are the primary input dataset that the Chloris Platform consumes. Site boundaries are vector datasets that describe the location and shape of a site. The Chloris Platform accepts site boundaries in most single-file formats, plus Shapefiles which have been zipped. Additionally, in the platform interface, you can draw your own site boundaries using the interactive map.

In order to assure the quality of our results, we enforce limits on the minimum size of sites and individual polygons (for statistical accuracy), how sparse the site may be (especially when given multi-polygons), and maximum precision of the vertices. If your site boundaries do not meet these requirements, you will receive a detailed error message explaining why.

Site boundary projection

The Chloris Platform automatically re-projects each site boundary to the most appropriate UTM-based projection based on the center of the bounding box of the boundary. Therefore, you do not typically need to re-project your boundaries prior to submitting them if you are using a common projection. A notable exception to this are LCC projections, which we recommend re-projecting to WGS84 (EPSG:4326) and verifying the results ahead of time. See computing projections / areas for more info.

Output datasets

For each site, the following datasets are produced:

Normalized boundary polygon

For your review and convenience, the Chloris Platform will return the normalized site boundary that was used for analysis with any invalid geometries (i.e. points and lines) removed, and all polygons flattened into a single multi polygon. Curve geometries are not supported in the platform and are approximated using linear segments. After a site has been submitted, you can retrieve the normalized boundary polygon using the boundaryPath field in the reporting unit entry.

Representative point

For map navigation purposes, the Chloris Platform will return a point (X,Y) that is representative of the reporting unit in WGS84 lon/lat when listing reporting units in the fields pointX and pointY. This point is guaranteed to be inside the reporting unit boundary geometry, so it is safe to perform spatial joins with (but may not be the exact centroid). If the same site is submitted multiple times, the representative points will be identical.

GeoTiff raster file downloads

If your licensing agreement includes data download, the data products (listed below) are available for download as GeoTiff files. Annual data products have one file per year. Difference data products are relative to the previous year, so change from 2000 to 2001 is reported in the 2001 file and there is no 2000 change file. Note that "t" refers to metric tonnes and "ha" refers to hectares.

Available datasets:

  • stock: Annual above-ground biomass stock in the given year (t/ha).
  • change: Annual above-ground biomass change (t/ha/yr).
  • se: Standard error of the annual above-ground biomass stock estimates (t/ha)*.
  • change_se: Standard error of the annual above-ground biomass change estimates (t/ha/yr)*.
  • forest: Area which is forest in the selected year. Boolean (0 or 1) mask.
  • forest_area_gain: Area which is forest in the given year, but not the previous year.
  • forest_area_loss: Area which is not forest in the given year, but was forest in the previous year.
  • forest_area_degradation: Area which remained forest in the given year, but experienced statistically significant losses in biomass.

* Introduced 2024-01-17.

An index of the files available to download is available at {dataPath}/downloads.json where dataPath is specified on the reporting unit entry.

The contents of the downloads index follow this schema:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "downloads": {
      "type": "array",
      "items": {
        "description": "An entry for a single downloadable file.",
        "type": "object",
        "properties": {
          "format": {
            "type": "string",
            "enum": ["tif", "geojson", "zip"]
          },
          "date": {
            "description": "The date of the data in the file.",
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "urlFormat": {
            "type": "string",
            "description": "The key in the urlFormats object which this url conforms to. Optional, as not all urls will conform to a urlFormat (e.g. a project-level summary pdf report)."
          },
          "sizeMB": {
            "description": "The approximate size of the file in megabytes.",
            "type": "number"
          }
        },
        "required": ["format", "date", "url"]
      }
    },
    "urlFormats": {
      "description": "A map of url formats which may be used to parse variables out of urls.",
      "type": "object",
      "additionalProperties": {
        "type": "string"
      }
    },
    "formatVersion": {
      "type": "string",
      "enum": ["0.1"]
    }
  },
  "required": ["formatVersion", "downloads", "urlFormats"]
}

Example downloads index:

{
  "formatVersion": "0.1",
  "downloads": [
    {
      "format": "tif",
      "date": "2023-01-01",
      "url": "s3://chloris-app-data/data/abc01234/efg56789/downloads/merged_2000_95a.tif",
      "urlFormat": "stock"
    },
    {
      "format": "tif",
      "date": "2023-01-01",
      "url": "s3://chloris-app-data/data/abc01234/efg56789/downloads/merged_2001_ch95a.tif",
      "urlFormat": "change"
    },
    {
      "format": "geojson",
      "date": "2023-01-01",
      "url": "s3://chloris-app-data/data/abc01234/efg56789/downloads/efg56789_boundary.geojson",
      "urlFormat": "boundary"
    }
  ],
  "urlFormats": {
    "stock": "s3://chloris-app-data/data/{organizationId}/{reportingUnitId}/downloads/merged_{year}_95a.tif",
    "change": "s3://chloris-app-data/data/{organizationId}/{reportingUnitId}/downloads/merged_{year}_ch95a.tif",
    "boundary": "s3://chloris-app-data/data/{organizationId}/{reportingUnitId}/downloads/{reportingUnitId}_boundary.geojson"
  }
}

This means you can directly iterate the list for urls, or parse out arbitrary variables for your own processing needs. Note that chloris-app-data is a private bucket accessible only through app.chloris.earth, so you will need to replace the s3://chloris-app-data/ prefix with https://app.chloris.earth/ to download the files.

Here is an example of how to download all the stock rasters for a site using the chloris-app-sdk in Python:

# download_stock_rasters.py
import urllib3
from chloris_app_sdk import ChlorisAppClient

http = urllib3.PoolManager()

# create the client
client = ChlorisAppClient(
    organization_id="your-organization-id",
    refresh_token="your-refresh-token", # env: CHLORIS_REFRESH_TOKEN
    # or
    # id_token="your-id-token" # env: CHLORIS_ID_TOKEN
)

reporting_unit_id = 'abcd1234'

# get the reporting unit entry with downloads (if available)
reporting_unit_entry = client.get_reporting_unit(reporting_unit_id, include_stats=False, include_downloads=True)

if not reporting_unit_entry.get('downloads'):
    raise Exception('No downloads available for this reporting unit')

downloads_index = reporting_unit_entry['downloads']

# Filter for stock rasters
stock_urls = [d['url'] for d in downloads_index['downloads'] if d['urlFormat'] == 'stock']

print(f'Downloading {len(stock_urls)} stock rasters for reporting unit {reporting_unit_id}')
for url in stock_urls:
    r = http.request('GET', url.replace('s3://chloris-app-data/', 'https://app.chloris.earth/'))
    fname = url.split('/')[-1]
    with open(fname, 'wb') as f:
        f.write(r.data)
    print(f'Downloaded {fname}')

Here is an example of how to download all the stock rasters for a site in Javascript (node):

// downloadStockRasters.mjs
import fetch from "node-fetch";

const organizationId = "your-organization-id";
const reportingUnitId = "abcd1234";

const reporting_unit_entry_response = await fetch(`https://app.chloris.earth/api/reportingUnit/`, {
  headers: {
    Authorization: `Bearer ${idToken}`,
  },
  body: JSON.stringify({
    organizationId,
    reportingUnitId,
  }),
});
const reporting_unit_entry = await reporting_unit_entry_response.json();

const dataPath = reporting_unit_entry.dataPath.replace("s3://chloris-app-data/", "https://app.chloris.earth/");

const url = `${dataPath}/downloads.json`;

const downloads_response = await fetch(url);
const downloads_response_json = await downloads_response.json();

const stockUrls = downloads_response_json.downloads.filter((d) => d.urlFormat === "stock").map((d) => d.url);

for (const url of stockUrls) {
  const response = await fetch(url);
  const buffer = await response.buffer();
  const fname = url.split("/").pop();
  await fs.promises.writeFile(fname, buffer);
  console.log(`Downloaded ${fname}`);
}

Computing projections / areas

The Chloris Platform uses the UTM-based projections for each site boundary when computing the area of the site. The following sample code computes the UTM zone number and EPSG code for a given geopandas DataFrame in Python:

import math
import geopandas as gpd

def get_utm_zone(lon: float, lat: float) -> int:
    """ Get UTM zone number from latitude and longitude """
    if 72.0 <= lat < 84.0:
        if 0.0 <= lon < 9.0:
            return 31
        if 9.0 <= lon < 21.0:
            return 33
        if 21.0 <= lon < 33.0:
            return 35
        if 33.0 <= lon < 42.0:
            return 37
    if 56 <= lat < 64.0 and 3 <= lon <= 12:
        return 32
    return math.floor((lon + 180) / 6) + 1


def get_utm_epsg(lon: float, lat: float) -> int:
    """ Find the UTM zone for a given longitude and latitude and return the EPSG code. """
    zone = get_utm_zone(lon, lat)

    epsg_code = 32600
    epsg_code += int(zone)
    if lat < 0:  # Southern hemisphere
        epsg_code += 100
    return epsg_code


def get_gdf_area_km2(gdf: gpd.GeoDataFrame) -> float:
    """ Calculate the total area of a GeoDataFrame in km2 using the center of its bounding box. """
    bbox = gdf.to_crs(epsg=4326).total_bounds
    epsg = get_utm_epsg(lat=(bbox[1] + bbox[3]) / 2, lon=(bbox[0] + bbox[2]) / 2)
    gdf = gdf.to_crs(epsg=epsg)

    # calculate area in km2
    gdf["area_km2"] = gdf.area / 1e6
    return gdf["area_km2"].sum()