time series API¶
The timeseries
extension provides endpoints for requesting results for all points or intervals
along a time series. The /timeseries
family of endpoints works by converting
the provided time series parameters (datetime
, step
, and temporal_mode
) into a set of
datetime
query parameters for the corresponding lower-level endpoint, running asynchronous
requests to the lower-level endpoint, then collecting the results and formatting them in a coherent
format for the user.
The time series structure is defined by the datetime
, step
, and temporal_mode
parameters.
The temporal_mode
mode parameter controls whether or not CMR is queried for a particular
point-in-time (temporal_mode=point
) or over an entire interval (temporal_mode=interval
).
In general, it is best to use temporal_mode=point
for datasets where granules overlap completely
in space (e.g. daily sea surface temperature predictions) because the /timeseries endpoints will
create a mosaic of all assets returned by the query and the first asset to cover a pixel will
be used. For datasets where it requires granules from multiple timestamps to fully cover an AOI,
temporal_mode=interval
is appropriate. For example, you can get weekly composites of satellite
imagery for visualization purposes with step=P1W & temporal_mode=interval
.
from IPython.display import IFrame
# if running titiler-cmr in the docker network
# titiler_endpoint = "http://localhost:8081"
# titiler_endpoint = "http://localhost:8081" # docker network endpoint
titiler_endpoint = (
"https://staging.openveda.cloud/api/titiler-cmr" # VEDA staging endpoint
)
# titiler_endpoint = "https://v4jec6i5c0.execute-api.us-west-2.amazonaws.com" # dev endpoint
IFrame(f"{titiler_endpoint}/api.html#Timeseries", 900, 500)
import json
from datetime import datetime
import httpx
import matplotlib.pyplot as plt
import numpy as np
from folium import LayerControl, Map, TileLayer
from geojson_pydantic import Feature, Polygon
from IPython.display import Image, display
time series parameters¶
The time series API makes it possible to return results for many points along a timeseries with a single request. The available parameters are:
datetime
(str): Either a date-time, an interval, or a comma-separated list of date-times or intervals. Date and time expressions adhere to rfc3339 ('2020-06-01T09:00:00Z') format.step
(str): width of individual time steps expressed as a IS8601 durationtemporal_mode
(str): if"point"
, queries will be made for the individual timestamps along the timeseries. If"interval"
, queries will be made for the periods between each timestamp along the timeseries.
There are many ways to combine the parameters to produce a time series.
- Exact points in time from a start to and end datetime:
- provide
datetime={start_datetime}/{end_datetime}
,step={step_width}
, andtemporal_mode=point
wherestep_width
is something likeP1D
for daily orP2W
for bi-weekly. - provide
datetime={start_datetime}/{end_datetime}
, andtemporal_mode=point
withoutstep
to get a point for every unique timestamp in the granules betweenstart_datetime
andend_datetime
.
- Fixed-width intervals between a start and end datetime:
- provide
datetime={start_datetime}/{end_datetime}
,step
, andtemporal_mode=interval
- Specific datetimes
- provide
datetime=2024-10-01T00:00:01Z,2024-10-02T00:00:01Z
- Specific datetime intervals
- provide
datetime=2024-10-01T00:00:01Z/2024-10-01T23:59:59Z,2024-10-05T00:00:01Z/2024-10-05T23:59:59Z
How to use the timeseries API with titiler.cmr
¶
The /timeseries
endpoints work by interpreting the time series parameters (e.g. datetime
and step
) and parameterizing a set of lower-level requests to the related endpoint. For example, a request to /timeseries/statistics
for a set of four points in time each one week apart will fire off four requests to the /statistics
endpoint with a particular value in the datetime
parameter. The results are collected and returned in a coherent format that can be consumed in a table or a chart.
Every /timeseries
request in titiler.cmr
will require both a concept_id
and a set of time series parameters. The GHRSST Level 4 GAMSSA_28km Global Foundation Sea Surface Temperature Analysis v1.0 dataset (GDS2) is a useful dataset for demo purposes because the granule assets are small (~1MB each).
See Time series API limits for details on limits to the spatial and temporal extent for requests to a titiler-cmr
deployment.
concept_id = "C2036881735-POCLOUD"
The /timeseries
GET
endpoint is useful for demonstrating how the timeseries family of endpoints constructs sub-requests. It returns the list of titiler.cmr
query parameters (datetime
and concept_id
) that will be used to generate the timeseries results.
Time series for all granules between a start/end datetime¶
For some datasets that have granules that are regularly spaced in time (e.g. daily), it is useful to be able to quickly specify a summary of all points in time between a start and end datetime. You can do that by simply providing the start_datetime
and end_datetime
parameters. The application will query CMR and produce a list of unique datetime
values from the results of the granule search. If a granule represents a datetime range, it will return the midpoint between the start and end for a single granule.
response = httpx.get(
f"{titiler_endpoint}/timeseries",
params={
"concept_id": concept_id,
"datetime": "2024-10-01T00:00:01Z/2024-10-05T00:00:01Z",
},
timeout=None,
).json()
print(json.dumps(response, indent=2))
[ { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-01T12:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-02T12:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-03T12:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-04T12:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-05T12:00:00+00:00" } ]
Weekly timeseries¶
Sometimes you might be interested in a report with lower temporal resolution than the maximum availble for a dataset. By setting step="P1W"
and temporal_mode="point"
, you can get a weekly series.
response = httpx.get(
f"{titiler_endpoint}/timeseries",
params={
"concept_id": concept_id,
"datetime": "2024-10-01T00:00:01Z/2024-10-30T00:00:01Z",
"step": "P1W",
"temporal_mode": "point",
},
).json()
print(json.dumps(response, indent=2))
[ { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-01T00:00:01+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-08T00:00:01+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-15T00:00:01+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-22T00:00:01+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-29T00:00:01+00:00" } ]
Periodic timeseries¶
Some datasets (like satellite imagery) may consist of granules that do not fully cover an arbitrary area of interest. In this case it is useful to construct a time series from a set of datetime ranges so that granules can be mosaiced to ensure each step has full coverage.
To create a set of non-overlapping week-long datetime ranges, you can modify the query to use temporal_mode="interval"
which will create ranges that start on the weekly values returned in the previous query and extend up to the second before the next value in the series.
response = httpx.get(
f"{titiler_endpoint}/timeseries",
params={
"concept_id": concept_id,
"datetime": "2024-10-01T00:00:01Z/2024-10-30T00:00:01Z",
"step": "P1W",
"temporal_mode": "interval",
},
).json()
print(json.dumps(response, indent=2))
[ { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-01T00:00:01+00:00/2024-10-08T00:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-08T00:00:01+00:00/2024-10-15T00:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-15T00:00:01+00:00/2024-10-22T00:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-22T00:00:01+00:00/2024-10-29T00:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-29T00:00:01+00:00/2024-10-30T00:00:01+00:00" } ]
Custom time series¶
If you want to specify the exact datetime values for a time series and you either cannot do not want to use the time series parameters, you can supply a set of comma-separated datetimes and/or datetime ranges to the datetime
parameter.
response = httpx.get(
f"{titiler_endpoint}/timeseries",
params={
"concept_id": concept_id,
"datetime": ",".join(
["2024-10-01T00:00:01Z", "2024-10-07T00:00:01Z/2024-10-09T23:59:59Z"]
),
},
).json()
print(json.dumps(response, indent=2))
--------------------------------------------------------------------------- ReadTimeout Traceback (most recent call last) File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpx/_transports/default.py:101, in map_httpcore_exceptions() 100 try: --> 101 yield 102 except Exception as exc: File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpx/_transports/default.py:250, in HTTPTransport.handle_request(self, request) 249 with map_httpcore_exceptions(): --> 250 resp = self._pool.handle_request(req) 252 assert isinstance(resp.stream, typing.Iterable) File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpcore/_sync/connection_pool.py:256, in ConnectionPool.handle_request(self, request) 255 self._close_connections(closing) --> 256 raise exc from None 258 # Return the response. Note that in this case we still have to manage 259 # the point at which the response is closed. File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpcore/_sync/connection_pool.py:236, in ConnectionPool.handle_request(self, request) 234 try: 235 # Send the request on the assigned connection. --> 236 response = connection.handle_request( 237 pool_request.request 238 ) 239 except ConnectionNotAvailable: 240 # In some cases a connection may initially be available to 241 # handle a request, but then become unavailable. 242 # 243 # In this case we clear the connection and try again. File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpcore/_sync/connection.py:103, in HTTPConnection.handle_request(self, request) 101 raise exc --> 103 return self._connection.handle_request(request) File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpcore/_sync/http11.py:136, in HTTP11Connection.handle_request(self, request) 135 self._response_closed() --> 136 raise exc File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpcore/_sync/http11.py:106, in HTTP11Connection.handle_request(self, request) 97 with Trace( 98 "receive_response_headers", logger, request, kwargs 99 ) as trace: 100 ( 101 http_version, 102 status, 103 reason_phrase, 104 headers, 105 trailing_data, --> 106 ) = self._receive_response_headers(**kwargs) 107 trace.return_value = ( 108 http_version, 109 status, 110 reason_phrase, 111 headers, 112 ) File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpcore/_sync/http11.py:177, in HTTP11Connection._receive_response_headers(self, request) 176 while True: --> 177 event = self._receive_event(timeout=timeout) 178 if isinstance(event, h11.Response): File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpcore/_sync/http11.py:217, in HTTP11Connection._receive_event(self, timeout) 216 if event is h11.NEED_DATA: --> 217 data = self._network_stream.read( 218 self.READ_NUM_BYTES, timeout=timeout 219 ) 221 # If we feed this case through h11 we'll raise an exception like: 222 # 223 # httpcore.RemoteProtocolError: can't handle event type (...) 227 # perspective. Instead we handle this case distinctly and treat 228 # it as a ConnectError. File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpcore/_backends/sync.py:126, in SyncStream.read(self, max_bytes, timeout) 125 exc_map: ExceptionMapping = {socket.timeout: ReadTimeout, OSError: ReadError} --> 126 with map_exceptions(exc_map): 127 self._sock.settimeout(timeout) File /usr/lib/python3.12/contextlib.py:158, in _GeneratorContextManager.__exit__(self, typ, value, traceback) 157 try: --> 158 self.gen.throw(value) 159 except StopIteration as exc: 160 # Suppress StopIteration *unless* it's the same exception that 161 # was passed to throw(). This prevents a StopIteration 162 # raised inside the "with" statement from being suppressed. File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpcore/_exceptions.py:14, in map_exceptions(map) 13 if isinstance(exc, from_exc): ---> 14 raise to_exc(exc) from exc 15 raise ReadTimeout: The read operation timed out The above exception was the direct cause of the following exception: ReadTimeout Traceback (most recent call last) Cell In[7], line 1 ----> 1 response = httpx.get( 2 f"{titiler_endpoint}/timeseries", 3 params={ 4 "concept_id": concept_id, 5 "datetime": ",".join( 6 ["2024-10-01T00:00:01Z", "2024-10-07T00:00:01Z/2024-10-09T23:59:59Z"] 7 ), 8 }, 9 ).json() 11 print(json.dumps(response, indent=2)) File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpx/_api.py:195, in get(url, params, headers, cookies, auth, proxy, follow_redirects, verify, timeout, trust_env) 174 def get( 175 url: URL | str, 176 *, (...) 185 trust_env: bool = True, 186 ) -> Response: 187 """ 188 Sends a `GET` request. 189 (...) 193 on this function, as `GET` requests should not include a request body. 194 """ --> 195 return request( 196 "GET", 197 url, 198 params=params, 199 headers=headers, 200 cookies=cookies, 201 auth=auth, 202 proxy=proxy, 203 follow_redirects=follow_redirects, 204 verify=verify, 205 timeout=timeout, 206 trust_env=trust_env, 207 ) File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpx/_api.py:109, in request(method, url, params, content, data, files, json, headers, cookies, auth, proxy, timeout, follow_redirects, verify, trust_env) 57 """ 58 Sends an HTTP request. 59 (...) 100 ``` 101 """ 102 with Client( 103 cookies=cookies, 104 proxy=proxy, (...) 107 trust_env=trust_env, 108 ) as client: --> 109 return client.request( 110 method=method, 111 url=url, 112 content=content, 113 data=data, 114 files=files, 115 json=json, 116 params=params, 117 headers=headers, 118 auth=auth, 119 follow_redirects=follow_redirects, 120 ) File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpx/_client.py:827, in Client.request(self, method, url, content, data, files, json, params, headers, cookies, auth, follow_redirects, timeout, extensions) 812 warnings.warn(message, DeprecationWarning, stacklevel=2) 814 request = self.build_request( 815 method=method, 816 url=url, (...) 825 extensions=extensions, 826 ) --> 827 return self.send(request, auth=auth, follow_redirects=follow_redirects) File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpx/_client.py:916, in Client.send(self, request, stream, auth, follow_redirects) 912 self._set_timeout(request) 914 auth = self._build_request_auth(request, auth) --> 916 response = self._send_handling_auth( 917 request, 918 auth=auth, 919 follow_redirects=follow_redirects, 920 history=[], 921 ) 922 try: 923 if not stream: File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpx/_client.py:944, in Client._send_handling_auth(self, request, auth, follow_redirects, history) 941 request = next(auth_flow) 943 while True: --> 944 response = self._send_handling_redirects( 945 request, 946 follow_redirects=follow_redirects, 947 history=history, 948 ) 949 try: 950 try: File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpx/_client.py:981, in Client._send_handling_redirects(self, request, follow_redirects, history) 978 for hook in self._event_hooks["request"]: 979 hook(request) --> 981 response = self._send_single_request(request) 982 try: 983 for hook in self._event_hooks["response"]: File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpx/_client.py:1016, in Client._send_single_request(self, request) 1011 raise RuntimeError( 1012 "Attempted to send an async request with a sync Client instance." 1013 ) 1015 with request_context(request=request): -> 1016 response = transport.handle_request(request) 1018 assert isinstance(response.stream, SyncByteStream) 1020 response.request = request File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpx/_transports/default.py:249, in HTTPTransport.handle_request(self, request) 235 import httpcore 237 req = httpcore.Request( 238 method=request.method, 239 url=httpcore.URL( (...) 247 extensions=request.extensions, 248 ) --> 249 with map_httpcore_exceptions(): 250 resp = self._pool.handle_request(req) 252 assert isinstance(resp.stream, typing.Iterable) File /usr/lib/python3.12/contextlib.py:158, in _GeneratorContextManager.__exit__(self, typ, value, traceback) 156 value = typ() 157 try: --> 158 self.gen.throw(value) 159 except StopIteration as exc: 160 # Suppress StopIteration *unless* it's the same exception that 161 # was passed to throw(). This prevents a StopIteration 162 # raised inside the "with" statement from being suppressed. 163 return exc is not value File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpx/_transports/default.py:118, in map_httpcore_exceptions() 115 raise 117 message = str(exc) --> 118 raise mapped_exc(message) from exc ReadTimeout: The read operation timed out
Example: sea surface temperature GIF¶
The /timeseries/bbox
endpoint can be used to produce a GIF that shows a visualization of granules over time.
The example below shows biweekly sea surface temperature estimates from the GAMSSA dataset for the period from November 2023 through October 2024.
minx, miny, maxx, maxy = -180, -90, 180, 90
request = httpx.get(
f"{titiler_endpoint}/timeseries/bbox/{minx},{miny},{maxx},{maxy}.gif",
params={
"concept_id": concept_id,
"datetime": "2023-11-01T00:00:01Z/2024-10-30T23:59:59Z",
"step": "P2W",
"temporal_mode": "point",
"variable": "analysed_sst",
"backend": "xarray",
"colormap_name": "thermal",
"rescale": [[273, 315]],
},
timeout=None,
)
display(Image(request.content))
Example: HLSL30 GIF¶
The example below shows a weekly mosaic of imagery from the Harmonized Landsat Sentinel L30 (HLSL30) collection for the period from July through October 2021 in the area of the Greenwood Fire.
minx, miny, maxx, maxy = -91.816, 47.491, -91.359, 47.716
request = httpx.get(
f"{titiler_endpoint}/timeseries/bbox/{minx},{miny},{maxx},{maxy}/512x512.gif",
params={
"concept_id": "C2021957657-LPCLOUD",
"datetime": "2021-07-01T00:00:00Z/2021-10-30T00:00:00Z",
"step": "P5D",
"temporal_mode": "interval",
"backend": "rasterio",
"bands_regex": "B[0-9][0-9]",
"bands": ["B04", "B03", "B02"],
"color_formula": "Gamma RGB 3.5 Saturation 1.7 Sigmoidal RGB 15 0.35",
"fps": 2,
},
timeout=None,
)
display(Image(request.content))
Example: sea surface temperature statistics¶
The /timeseries/statistics
endpoint will produce summary statistics for an AOI for all points along a timeseries.
The example below shows daily sea surface temperature summary statistics for the Gulf of Mexico from the GAMSSA dataset for the period from March 2022 through October 2024.
%%time
minx, miny, maxx, maxy = -98.676, 18.857, -81.623, 31.097
geojson = Feature(
type="Feature",
geometry=Polygon.from_bounds(minx, miny, maxx, maxy),
properties={},
)
request = httpx.post(
f"{titiler_endpoint}/timeseries/statistics",
params={
"concept_id": concept_id,
"datetime": "2022-03-01T00:00:01Z/2024-10-30T23:59:59Z",
"step": "P1D",
"temporal_mode": "point",
"variable": "analysed_sst",
"backend": "xarray",
},
json=geojson.model_dump(exclude_none=True),
timeout=None,
)
request.raise_for_status()
response = request.json()
--------------------------------------------------------------------------- HTTPStatusError Traceback (most recent call last) File <timed exec>:21 File ~/work/titiler-cmr/titiler-cmr/.venv/lib/python3.12/site-packages/httpx/_models.py:829, in Response.raise_for_status(self) 827 error_type = error_types.get(status_class, "Invalid status code") 828 message = message.format(self, error_type=error_type) --> 829 raise HTTPStatusError(message, request=request, response=self) HTTPStatusError: Server error '504 Gateway Timeout' for url 'https://staging.openveda.cloud/api/titiler-cmr/timeseries/statistics?concept_id=C2036881735-POCLOUD&datetime=2022-03-01T00%3A00%3A01Z%2F2024-10-30T23%3A59%3A59Z&step=P1D&temporal_mode=point&variable=analysed_sst&backend=xarray' For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504
The /timeseries/statistics
endpoint returns the GeoJSON with statistics for each step in the time series embedded in the properties.
stats = response["properties"]["statistics"]
print(len(stats))
stats_preview = {
timestamp: sst_stats
for i, (timestamp, sst_stats) in enumerate(stats.items())
if i < 2
}
print(json.dumps(stats_preview, indent=2))
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[11], line 1 ----> 1 stats = response["properties"]["statistics"] 2 print(len(stats)) 4 stats_preview = { 5 timestamp: sst_stats 6 for i, (timestamp, sst_stats) in enumerate(stats.items()) 7 if i < 2 8 } TypeError: list indices must be integers or slices, not str
The statistics output can be used to generate plots like this:
data = response["properties"]["statistics"]
dates = []
means = []
stds = []
for date_str, values in data.items():
dates.append(datetime.fromisoformat(date_str))
means.append(values["analysed_sst"]["mean"])
stds.append(values["analysed_sst"]["std"])
plt.figure(figsize=(10, 6))
plt.plot(dates, means, "b-", label="Mean")
plt.fill_between(
dates,
np.array(means) - np.array(stds),
np.array(means) + np.array(stds),
alpha=0.2,
color="b",
label="Standard Deviation",
)
plt.xlabel("Date")
plt.ylabel("Temperature (K)")
plt.title("Mean sea surface temperature in the Gulf of Mexico")
plt.legend()
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[12], line 1 ----> 1 data = response["properties"]["statistics"] 3 dates = [] 4 means = [] TypeError: list indices must be integers or slices, not str
Example: Time series raster tiles¶
It could be useful to allow users to select a timestep in an interactive map. You can use the /timeseries/tilejson
endpoint for that purpose. The following example shows how you could use it to provide time series capability to an interactive map of sea ice cover.
minx, miny, maxx, maxy = -180, -90, 180, 90
request = httpx.get(
f"{titiler_endpoint}/timeseries/WebMercatorQuad/tilejson.json",
params={
"concept_id": concept_id,
"datetime": "2023-11-01T00:00:01Z/2024-10-30T23:59:59Z",
"step": "P1M",
"temporal_mode": "point",
"variable": "sea_ice_fraction",
"backend": "xarray",
"colormap_name": "blues_r",
"rescale": [[0, 1]],
},
timeout=None,
)
tilejsons = request.json()
tilejson_preview = {
timestamp: tilejson
for i, (timestamp, tilejson) in enumerate(tilejsons.items())
if i < 2
}
print(json.dumps(tilejson_preview, indent=2))
{ "2023-11-01T00:00:01+00:00": { "tilejson": "2.2.0", "version": "1.0.0", "scheme": "xyz", "tiles": [ "https://staging.openveda.cloud/api/titiler-cmr/tiles/WebMercatorQuad/{z}/{x}/{y}@1x?concept_id=C2036881735-POCLOUD&variable=sea_ice_fraction&backend=xarray&colormap_name=blues_r&rescale=%5B0%2C+1%5D&concept_id=C2036881735-POCLOUD&datetime=2023-11-01T00%3A00%3A01%2B00%3A00" ], "minzoom": 0, "maxzoom": 24, "bounds": [ -180.0, -90.0, 180.0, 90.0 ], "center": [ 0.0, 0.0, 0 ] }, "2023-12-01T00:00:01+00:00": { "tilejson": "2.2.0", "version": "1.0.0", "scheme": "xyz", "tiles": [ "https://staging.openveda.cloud/api/titiler-cmr/tiles/WebMercatorQuad/{z}/{x}/{y}@1x?concept_id=C2036881735-POCLOUD&variable=sea_ice_fraction&backend=xarray&colormap_name=blues_r&rescale=%5B0%2C+1%5D&concept_id=C2036881735-POCLOUD&datetime=2023-12-01T00%3A00%3A01%2B00%3A00" ], "minzoom": 0, "maxzoom": 24, "bounds": [ -180.0, -90.0, 180.0, 90.0 ], "center": [ 0.0, 0.0, 0 ] } }
m = Map(location=[0, 0], zoom_start=3, min_zoom=3)
for datetime_, tilejson in tilejsons.items():
label = datetime.fromisoformat(datetime_).strftime("%Y-%m")
TileLayer(
tiles=tilejson["tiles"][0],
attr="GAMSSA SST",
overlay=True,
name=label,
show=False,
).add_to(m)
LayerControl(collapsed=False).add_to(m)
m