1import requests
2import geopandas as gpd
3import pandas as pd
4import numpy as np
5# to display the return json
6import json
7from IPython.display import display, JSON, GeoJSON
8
9# import libs for plotting with lonboard
10from lonboard import Map, PolygonLayer
11import ipywidgets as widgets
12import matplotlib.pyplot as plt
13import matplotlib.ticker as mticker
14import matplotlib as mpl
15
16# function to create the color bar for plotting with lonboard
17def create_colorbar(data, cmap_name, legend, vcenter=None):
18 fig, ax = plt.subplots(figsize=(8, 0.8))
19 center = (np.nanmax(data)-np.nanmin(data))/2 if (vcenter is None) else vcenter
20 cbar = plt.colorbar(
21 plt.cm.ScalarMappable(norm=mpl.colors.TwoSlopeNorm(vmin=np.nanmin(data), vmax=np.nanmax(data), vcenter=center), cmap=cmap_name),
22 cax=ax,
23 orientation='horizontal'
24 )
25 tick_values = np.linspace(np.nanmin(data), np.nanmax(data), num=10)
26 cbar.set_ticks(tick_values)
27 #cbar.ax.xaxis.set_major_formatter(mticker.FuncFormatter(thousands_formatter))
28 cbar.ax.tick_params(labelsize=8)
29 cbar.set_label(legend, fontsize=10)
30 plt.tight_layout()
31 plt.show()
A demo notebook for pydggsapi
Description
Purpose: To discover DGGS collections using pydggsapi. In this example, we focus on 3 DGGS collections in IGEO7 DGGRS located in Estonia near Elva. They are :
DEM
Slope
TWI
All collections are produced from a clipped Geotiff near Elva with 10M spatial resolution. The clipped region was converted to DGGRS IGEO7 at refinement 14 using the nearest centroid regridding method. Then, data were aggregated to the refinement level 1 using the mean (skip nan), with the 1-to-7 hierarchical structure.
Those collections are hosted in the pydggsapi instance: https://dggs.geokuup.ee/dggs-api/.
The spatial extent of the region in wgs84 is : [[26.2718, 58.2267, 26.5815, 58.3295]]
1# define the root URL of the API endpoint
2dggs_api_root = "https://dggs.geokuup.ee/dggs-api"
Where is it?
The first step to discover collections published by the instance is to answer the question of “Where is it?” The answer is provided by the zone-query endpoint. The query accepts the following query parameters:
Zone-query endpoint details
bbox: Bounding box of the query. The bounding box is provided as four coordinates, in the order of minx, miny, maxx and maxyzone-level: For specifying a level at which to return a list of DGGRS zones using a zone_level query parameter.compact-zone: For specifying whether to retrieve a list of DGGRS zones using a compact_zones query parameter.parent-zone: For specifying a parent zone within which to restrict zone listing using a parent_zone query parameter.geometry: Specify the return geometry (zone-centroid or zone-region), defaults to zone-regionfilter: A CQL2 filter stringdatetime: Specify the datetime interval
Users can specify the return type in the HTTP header or using the format query string. The zone query supports the following return types, defaults to JSON:
?f=json: or setting application/json in the HTTP header, it returns the zones list in DGGS-JSON schema.?f=geojson: or setting application/geo+json in the HTTP header, it returns the zones list in geojson with geometries supplied.
It provides translation service between coordinate and DGGS zones ID. It returns a list of zones that consists of data from published collections.
Execute the Zone-query
1# To query the zones list of a bounding box at refinement level 7 (~62 km^2 per zone) with compact zone set to off
2bbox = "26.2718,58.2267,26.5815,58.3295"
3zone_level=7
4compact_zone=False
5# send the query , we are instereted in DGGRS IGEO7, so specify it in the path parameter
6zone_query_return = requests.get(dggs_api_root+'/dggs/igeo7/zones', params={'bbox': bbox,
7 'zone-level': zone_level,
8 'compact-zone': compact_zone})
9if (zone_query_return.status_code != 204):
10 zone_query_return = zone_query_return.json()
11 display(JSON(zone_query_return))
12else:
13 print("empty return")
- "root": {
- "returnedAreaMetersSquare": 495484142.40000004,
- "zones": [
- "000102244",
- "000102246",
- "000102630",
- "000102631",
- "000102633",
- "000102634",
- "000102635",
- "000102636"
Returns from the Zone-query
The Above returns 8 zones ID of IGEO7 at refinement level 7. It means that those zones consists of data from collections published in this pydggapi instance. Those zones ID are used to retrieval the actual data in the next step.
What is here?
After knowing where to look, it’s time to answer the question “What is here?” to get the actual data from collections using the zone-data retrieval endpoint.
Zone-data retrieval endpoint details
The query accepts the following query parameters:
zone-depth: For specifying a level at which to return a list of DGGRS zones using a zone_level query parameter.geometry: Specify the return geometry (zone-centroid or zone-region), defaults to zone-regionfilter: A CQL2 filter stringdatetime: Specify the datetime interval
Users can specify the return type in the HTTP header or using the format query string. The zone query supports the following return types, defaults to JSON:
?f=json: or setting application/json in the HTTP header (default)?f=ubjson: or application/ubjson in the HTTP header?f=geojson: or application/geo+json in the HTTP header?f=zarr: or application/zarr+zip in the HTTP header
Execute the Zone-data retrieval
The simplest query with only the zone ID
1zone_id = '000102244'
2# Zone depth = 0 means to return data at the same refinement level with the supplied zone ID, in this case, it is 7.
3zone_depth = 0
4zone_data_return = requests.get(dggs_api_root+f'/dggs/igeo7/zones/{zone_id}/data', params={'zone-depth': zone_depth})
5
6if (zone_data_return.status_code != 204):
7 zone_data_return = zone_data_return.json()
8 display(JSON(zone_data_return))
9else:
10 print("empty return")
- "root": {
- "$schema": "https://schemas.opengis.net/ogcapi/dggs/1.0/core/schemas/dggs-json/dggs-json.json",
- "depths": [
- 0
- "dggrs": "https://agile-giss.copernicus.org/articles/6/32/2025/",
- "schema": {
- "$id": null,
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "properties": {
- "Estonia_DEM_10M_Elva_igeo7_zarr.dem": {
- "format": "float32",
- "type": "number"
- "Estonia_Slope_10M_Elva_igeo7_zarr.slope": {
- "format": "float32",
- "type": "number"
- "Estonia_TWI_10M_Elva_igeo7_zarr.twi": {
- "format": "float32",
- "type": "number"
- "Estonia_DEM_10M_Elva_igeo7_zarr.dem":
- "title": "DGGS Zones Data Schema",
- "type": "object"
- "values": {
- "Estonia_DEM_10M_Elva_igeo7_zarr.dem": [
- {
- "data": [
- 73.05735778808594
- "depth": 0,
- "shape": {
- "count": 1,
- "dimensions": {},
- "subZones": 1
- "data":
- "Estonia_Slope_10M_Elva_igeo7_zarr.slope": [
- {
- "data": [
- 1.2631150484085083
- "depth": 0,
- "shape": {
- "count": 1,
- "dimensions": {},
- "subZones": 1
- "data":
- "Estonia_TWI_10M_Elva_igeo7_zarr.twi": [
- {
- "data": [
- 10.184617042541504
- "depth": 0,
- "shape": {
- "count": 1,
- "dimensions": {},
- "subZones": 1
- "data":
- "Estonia_DEM_10M_Elva_igeo7_zarr.dem":
- "zoneId": "000102244"
From the above return, the properties attribute shows that there are 3 collections that contain data of the zone 000102244. They are :
Estonia_TWI_10M_Elva_igeo7_zarr.twi
Estonia_Slope_10M_Elva_igeo7_zarr.slope
Estonia_DEM_10M_Elva_igeo7_zarr.dem
The actual data from the 3 collections are located inside the values attribute.
Seamless integration with other applications using GeoJSON return
pydggsapi supports returning results in GeoJSON format, which is widely used in the geoscience field, making it easy to integrate into any workflow that supports GeoJSON with minimal changes.
1zone_id = '000102244'
2zone_depth = 0
3# Specify the return type using `f=geojson`
4zone_data_return = requests.get(dggs_api_root+f'/dggs/igeo7/zones/{zone_id}/data?f=geojson', params={'zone-depth': zone_depth})
5if (zone_data_return.status_code != 204):
6 zone_data_return = zone_data_return.json()
7 display(JSON(zone_data_return))
8else:
9 print("empty return")
- "root": {
- "features": [
- {
- "geometry": {
- "coordinates": [
- [
- [
- 26.596394975217226,
- 58.35373403615539
- [
- 26.55415841674231,
- 58.31463644512665
- [
- 26.586644647852268,
- 58.26981106048124
- [
- 26.661221489372462,
- 58.26407041715636
- [
- 26.7035207426413,
- 58.30314100981717
- [
- 26.671180652526793,
- 58.347979244629585
- [
- 26.596394975217226,
- 58.35373403615539
- "type": "Polygon"
- "coordinates":
- "id": 0,
- "properties": {
- "Estonia_DEM_10M_Elva_igeo7_zarr.dem": 73.05735778808594,
- "Estonia_Slope_10M_Elva_igeo7_zarr.slope": 1.2631150484085083,
- "Estonia_TWI_10M_Elva_igeo7_zarr.twi": 10.184617042541504,
- "depth": 0,
- "zoneId": "000102244"
- "type": "Feature"
- "geometry":
- "type": "FeatureCollection"
- "features":
From the GeoJSON above, it returns all data from the 3 collections, including the zone geometry. Since all collections are in DGGRS IGEO7, the polygons are hexagons.
Query data for all zones at a finer refinement level
By specifying zone_depth with different values, the query can retrieve data from a finer refinement level.
After getting all the GeoJSON returns of all zones, we can ingest that data into a geopandas dataframe using the from_features function of geopandas.
1%%time
2# query data for all zones using the zones' ID returns from zone-query
3zone_id_list = zone_query_return['zones']
4# zone-depth = 3 means to return data at 3 finer levels relative to the refinement level of zone ID.
5# In this case, that is 7 + 3 = 10
6zone_depth = 3
7
8zone_data_df = []
9for zone_id in zone_id_list:
10 zone_data_return = requests.get(dggs_api_root+f'/dggs/igeo7/zones/{zone_id}/data?f=geojson',
11 params={'zone-depth': zone_depth})
12 print(f'Finished the zone-data query for zone: {zone_id}.')
13 if (zone_data_return.status_code != 204):
14 zone_data_return = zone_data_return.json()
15 # convert the geojson return to geopandas dataframe, then append it to zone_data_df
16 zone_data_df.append(gpd.GeoDataFrame.from_features(zone_data_return))
17 else:
18 print("empty return")
19
20# Concat all dataframes together
21zone_data_df = gpd.GeoDataFrame(pd.concat(zone_data_df, ignore_index=True)).set_crs('wgs84')
22zone_data_df = zone_data_df.drop_duplicates('zoneId')
23# Then visualise it using the explore function
24zone_data_df.explore(column='Estonia_TWI_10M_Elva_igeo7_zarr.twi')
Finished the zone-data query for zone: 000102244.
Finished the zone-data query for zone: 000102246.
Finished the zone-data query for zone: 000102630.
Finished the zone-data query for zone: 000102631.
Finished the zone-data query for zone: 000102633.
Finished the zone-data query for zone: 000102634.
Finished the zone-data query for zone: 000102635.
Finished the zone-data query for zone: 000102636.
CPU times: user 2.25 s, sys: 52.1 ms, total: 2.3 s
Wall time: 14.2 s
Query data at a finer refinement level and visualise it using lonboard
Let’s try a finer refinement level by setting the zone-depth parameter to 5; it will retrieve data at refinement level 12 (5 levels finer relative to the zone ID’s refinement level).
it takes ~5mins to get around 50_000 zones data
follows from the above section, the data is converted into a geopandas dataframe
but this time, it is visualised using lonboard.
1%%time
2# Using the return from zone-query
3zone_id_list = zone_query_return['zones']
4# Zone depth = 5 means that return data at 5 finer level relative to the refinement level of zone ID, in this case,
5# it is 7 + 5 = 12
6zone_depth = 5
7large_zone_data_df = []
8for zone_id in zone_id_list:
9 zone_data_return = requests.get(dggs_api_root+f'/dggs/igeo7/zones/{zone_id}/data?f=geojson', params={'zone-depth': zone_depth}, timeout=None)
10 if (zone_data_return.status_code != 204):
11 print(f'Finished the zone-data query for zone: {zone_id}.')
12 zone_data_return = zone_data_return.json()
13 # convert the geojson return to geopandas dataframe, then append it to zone_data_df
14 large_zone_data_df.append(gpd.GeoDataFrame.from_features(zone_data_return))
15 else:
16 print("empty return")
17# Then concat all dataframes together
18large_zone_data_df = gpd.GeoDataFrame(pd.concat(large_zone_data_df, ignore_index=True)).set_crs('wgs84')
19large_zone_data_df = large_zone_data_df.drop_duplicates('zoneId')
Finished the zone-data query for zone: 000102244.
Finished the zone-data query for zone: 000102246.
Finished the zone-data query for zone: 000102630.
Finished the zone-data query for zone: 000102631.
Finished the zone-data query for zone: 000102633.
Finished the zone-data query for zone: 000102634.
Finished the zone-data query for zone: 000102635.
Finished the zone-data query for zone: 000102636.
CPU times: user 2.67 s, sys: 44.6 ms, total: 2.72 s
Wall time: 5min 36s
1# plot the dataframe with lonboard
2# select the variable for plotting :
3# ['Estonia_DEM_10M_Elva_igeo7_zarr.dem' 'Estonia_Slope_10M_Elva_igeo7_zarr.slope', 'Estonia_TWI_10M_Elva_igeo7_zarr.twi']
4variable = 'Estonia_DEM_10M_Elva_igeo7_zarr.dem'
5variable_values = large_zone_data_df[variable]
6#create a colour map that is identical to the one that used in create_colorbar
7center = np.nanmin(variable_values)+((np.nanmax(variable_values)- np.nanmin(variable_values))/2)
8cm = plt.cm.ScalarMappable(norm=mpl.colors.TwoSlopeNorm(vmin=np.nanmin(variable_values),
9 vmax=np.nanmax(variable_values),
10 vcenter=center), cmap='viridis')
11layer = PolygonLayer.from_geopandas(
12 large_zone_data_df,
13 line_width_min_pixels=0.2,
14 get_fill_color= cm.to_rgba(variable_values, bytes=True, alpha=0.4)
15)
16m = Map(layer)
17colorbar_output = widgets.Output()
18with colorbar_output:
19 create_colorbar(variable_values, 'viridis', variable, center)
20widgets.VBox([m,colorbar_output])
Data filtering using the filter parameter
During research, a user may want to focus on a specific subset of the data, which can be achieved by using the filter parameter. The filter parameter accepts a CQL query string that applies to collections containing the variables in the query string.
In the following example, the use of the filter parameter is demonstrated to return data with (slope > 3)
1%%time
2# Using the return from zone-query
3zone_id_list = zone_query_return['zones']
4zone_depth = 5
5# In this example, only returns zones that with slope > 3
6cql_filter = '(slope > 3)'
7
8filtered_large_zone_data_df = []
9for zone_id in zone_id_list:
10 zone_data_return = requests.get(dggs_api_root + f'/dggs/igeo7/zones/{zone_id}/data?f=geojson',
11 params={'zone-depth': zone_depth,
12 'filter': cql_filter})
13 if (zone_data_return.status_code != 204):
14 print(f'Finished the zone-data query for zone: {zone_id}.')
15 zone_data_return = zone_data_return.json()
16 # convert the geojson return to geopandas dataframe, then append it to zone_data_df
17 filtered_large_zone_data_df.append(gpd.GeoDataFrame.from_features(zone_data_return))
18 else:
19 print("empty return")
20
21# Then concat all dataframes together
22filtered_large_zone_data_df = gpd.GeoDataFrame(pd.concat(filtered_large_zone_data_df, ignore_index=True)).set_crs('wgs84')
23filtered_large_zone_data_df = filtered_large_zone_data_df.drop_duplicates('zoneId')
Finished the zone-data query for zone: 000102244.
Finished the zone-data query for zone: 000102246.
Finished the zone-data query for zone: 000102630.
Finished the zone-data query for zone: 000102631.
Finished the zone-data query for zone: 000102633.
Finished the zone-data query for zone: 000102634.
Finished the zone-data query for zone: 000102635.
Finished the zone-data query for zone: 000102636.
CPU times: user 771 ms, sys: 32.3 ms, total: 803 ms
Wall time: 7min 17s
1# plot the dataframe with lonboard
2variable = 'Estonia_Slope_10M_Elva_igeo7_zarr.slope'
3variable_values = filtered_large_zone_data_df[variable]
4#create a colour map that is identical to the one that used in create_colorbar
5center = np.nanmin(variable_values)+((np.nanmax(variable_values)- np.nanmin(variable_values))/2)
6cm = plt.cm.ScalarMappable(norm=mpl.colors.TwoSlopeNorm(vmin=np.nanmin(variable_values),
7 vmax=np.nanmax(variable_values),
8 vcenter=center), cmap='viridis')
9layer = PolygonLayer.from_geopandas(
10 filtered_large_zone_data_df,
11 line_width_min_pixels=0.2, # minimum width when zoomed out
12 get_fill_color= cm.to_rgba(variable_values, bytes=True, alpha=0.4)
13)
14m = Map(layer)
15colorbar_output = widgets.Output()
16with colorbar_output:
17 create_colorbar(variable_values, 'viridis', variable, center)
18widgets.VBox([m,colorbar_output])
Using MVT tiles to query collection
In this example, we use the ipyleaflet library to display the MVT tiles published by pydggsapi for each collection.
The MVT tiles URL can be used in any other GIS desktop application, such as QGIS.
1# Install ipyleaflet
2#!pip install ipyleaflet
3#!jupyter nbextension enable --py --sys-prefix ipyleaflet
1from ipyleaflet import Map, VectorTileLayer
1#Try out other collections
2#https://dggs.geokuup.ee/tiles-api/Estonia_Slope_10M_Elva_igeo7_zarr/{z}/{x}/{y}
3#https://dggs.geokuup.ee/tiles-api/Estonia_DEM_10M_Elva_igeo7_zarr/{z}/{x}/{y}
4#https://dggs.geokuup.ee/tiles-api/Estonia_TWI_10M_Elva_igeo7_zarr/{z}/{x}/{y}
5planet_tiles_url=("https://dggs.geokuup.ee/tiles-api/Estonia_Slope_10M_Elva_igeo7_zarr/{z}/{x}/{y}?relative_depth=1")
6
7tile_layer=VectorTileLayer(url=planet_tiles_url)
8
9m = Map(
10 default_tiles=tile_layer,
11 zoom=10,
12 center=[58.3780,26.7290],
13 scroll_wheel_zoom=True)
14
15m.add_layer(tile_layer)
16m